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

JuliaLang / julia / 1301

09 Oct 2025 08:00PM UTC coverage: 75.842% (-0.9%) from 76.72%
1301

push

buildkite

web-flow
ccall: make distinction of pointer vs name a syntactic distinction (#59165)

We have long expected users to be explicit about the library name for
`ccall`, and the `@ccall` macro has even always enforced that. That
means users should have already been using explicit syntax, even though
it wasn't strictly enforced. And indeed, the other syntax forms weren't
handled reliably anyways (since doing so would require linearizing IR if
and only if the runtime values required it, which is not something that
is computable, and thus was often done wrong). This now intends to align
the runtime and compiler to expect only those syntax forms that we could
reliably handle in the past without errors, and adds explicit errors for
other cases, most of which we previously knew would be unreliable due to
reliance upon inference in making particular decisions for the
semantics. The `ccall` function is already very special since it is more
like a actual macro (it does not exist as a binding or value), so we can
make unusual syntax decisions like this, mirroring `@ccall` also.

This fixes #57931, mostly by restricting the set of things that are
allowed to the set of things that have an obvious and pre-existing
behavior to be guaranteed to do that behavior. The hope is to PkgEval
this to check if any packages are doing something unusual and see if we
even need to allow anything else.

This drops support for https://github.com/JuliaLang/julia/pull/37123,
since we were going to use that for LazyLibraries, be we decided that
approach was quite buggy and that PR would make static compilation quite
impossible to support, so we instead actually implemented LazyLibraries
with a different approach. It could be re-enabled, but we never had
correct lowering or inference support for it, so it is presumably still
unused.

The goal is to cause breakage only where the package authors really
failed to express intent with syntax, and otherwise to explicitly
maintain support by adding cases ... (continued)

20 of 35 new or added lines in 8 files covered. (57.14%)

721 existing lines in 81 files now uncovered.

60487 of 79754 relevant lines covered (75.84%)

7672511.81 hits per line

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

84.84
/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_REPL_hint(io::IO, ex::UndefVarError)
3✔
21
    var = ex.var
3✔
22
    if var === :or
3✔
23
        print(io, "\nSuggestion: Use `||` for short-circuiting boolean OR.")
×
24
    elseif var === :and
3✔
25
        print(io, "\nSuggestion: Use `&&` for short-circuiting boolean AND.")
×
26
    elseif var === :help
3✔
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
3✔
31
        print(io, "\nSuggestion: To exit Julia, use Ctrl-D, or type exit() and press enter.")
×
32
    end
33
end
34

35
function __init__()
4✔
36
    Base.REPL_MODULE_REF[] = REPL
4✔
37
    Base.Experimental.register_error_hint(UndefVarError_REPL_hint, UndefVarError)
4✔
38
    return nothing
4✔
39
end
40

41
using Base.Meta, Sockets, StyledStrings
42
using JuliaSyntaxHighlighting
43
import InteractiveUtils
44
import FileWatching
45
import Base.JuliaSyntax: kind, @K_str, @KSet_str, Tokenize.tokenize
46

47
export
48
    AbstractREPL,
49
    BasicREPL,
50
    LineEditREPL,
51
    StreamREPL
52

53
public TerminalMenus
54

55
import Base:
56
    AbstractDisplay,
57
    display,
58
    show,
59
    AnyDict,
60
    ==
61

62
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
63✔
63

64
using Base.Terminals
65

66
abstract type AbstractREPL end
67

68
include("options.jl")
69

70
include("LineEdit.jl")
71
using .LineEdit
72
import .LineEdit:
73
    CompletionProvider,
74
    HistoryProvider,
75
    add_history,
76
    complete_line,
77
    history_next,
78
    history_next_prefix,
79
    history_prev,
80
    history_prev_prefix,
81
    history_first,
82
    history_last,
83
    history_search,
84
    setmodifiers!,
85
    terminal,
86
    MIState,
87
    PromptState,
88
    mode_idx
89

90
include("SyntaxUtil.jl")
91
include("REPLCompletions.jl")
92
using .REPLCompletions
93

94
include("TerminalMenus/TerminalMenus.jl")
95
include("docview.jl")
96

97
include("Pkg_beforeload.jl")
98

99
@nospecialize # use only declared type signatures
100

101
answer_color(::AbstractREPL) = ""
×
102

103
const JULIA_PROMPT = "julia> "
104
const PKG_PROMPT = "pkg> "
105
const SHELL_PROMPT = "shell> "
106
const HELP_PROMPT = "help?> "
107

108
mutable struct REPLBackend
109
    "channel for AST"
110
    repl_channel::Channel{Any}
111
    "channel for results: (value, iserror)"
112
    response_channel::Channel{Any}
113
    "flag indicating the state of this backend"
114
    in_eval::Bool
115
    "transformation functions to apply before evaluating expressions"
116
    ast_transforms::Vector{Any}
117
    "current backend task"
118
    backend_task::Task
119

120
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
73✔
121
        new(repl_channel, response_channel, in_eval, ast_transforms)
122
end
123
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
48✔
124

125
# A reference to a backend that is not mutable
126
struct REPLBackendRef
127
    repl_channel::Channel{Any}
23✔
128
    response_channel::Channel{Any}
129
end
130
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
23✔
131

132
function destroy(ref::REPLBackendRef, state::Task)
133
    if istaskfailed(state)
22✔
134
        close(ref.repl_channel, TaskFailedException(state))
×
135
        close(ref.response_channel, TaskFailedException(state))
×
136
    end
137
    close(ref.repl_channel)
22✔
138
    close(ref.response_channel)
22✔
139
end
140

141
"""
142
    softscope(ex)
143

144
Return a modified version of the parsed expression `ex` that uses
145
the REPL's "soft" scoping rules for global syntax blocks.
146
"""
147
function softscope(@nospecialize ex)
370✔
148
    if ex isa Expr
370✔
149
        h = ex.head
239✔
150
        if h === :toplevel
239✔
151
            ex′ = Expr(h)
135✔
152
            map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
135✔
153
            return ex′
135✔
154
        elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
104✔
155
            return ex
2✔
156
        elseif h === :global && all(x->isa(x, Symbol), ex.args)
103✔
157
            return ex
1✔
158
        else
159
            return Expr(:block, Expr(:softscope, true), ex)
101✔
160
        end
161
    end
162
    return ex
131✔
163
end
164

165
# Temporary alias until Documenter updates
166
const softscope! = softscope
167

168
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
2✔
169
    @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✔
170
end
171

172
function has_ancestor(query::Module, target::Module)
2✔
173
    query == target && return true
38✔
174
    while true
30✔
175
        next = parentmodule(query)
30✔
176
        next == target && return true
30✔
177
        next == query && return false
28✔
178
        query = next
15✔
179
    end
15✔
180
end
181

182
retrieve_modules(::Module, ::Any) = (nothing,)
6✔
183
function retrieve_modules(current_module::Module, mod_name::Symbol)
96✔
184
    mod = try
96✔
185
        getproperty(current_module, mod_name)
96✔
186
    catch
187
        return (nothing,)
19✔
188
    end
189
    return (mod isa Module ? mod : nothing,)
77✔
190
end
191
retrieve_modules(current_module::Module, mod_name::QuoteNode) = retrieve_modules(current_module, mod_name.value)
41✔
192
function retrieve_modules(current_module::Module, mod_expr::Expr)
41✔
193
    if Meta.isexpr(mod_expr, :., 2)
41✔
194
        current_module = retrieve_modules(current_module, mod_expr.args[1])[1]
41✔
195
        current_module === nothing && return (nothing,)
41✔
196
        return (current_module, retrieve_modules(current_module, mod_expr.args[2])...)
41✔
197
    else
198
        return (nothing,)
×
199
    end
200
end
201

202
add_locals!(locals, ast::Any) = nothing
1✔
203
function add_locals!(locals, ast::Expr)
14✔
204
    for arg in ast.args
14✔
205
        add_locals!(locals, arg)
23✔
206
    end
23✔
207
    return nothing
14✔
208
end
209
function add_locals!(locals, ast::Symbol)
57✔
210
    push!(locals, ast)
57✔
211
    return nothing
57✔
212
end
213

214
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
1,167✔
215
    ast isa Expr || return
1,687✔
216

217
    # don't recurse through module definitions
218
    ast.head === :module && return
647✔
219

220
    if Meta.isexpr(ast, :., 2)
647✔
221
        mod_name, name_being_accessed = ast.args
61✔
222
        # retrieve the (possibly-nested) module being named here
223
        mods = retrieve_modules(current_module, mod_name)
61✔
224
        all(x -> x isa Module, mods) || return
169✔
225
        outer_mod = first(mods)
36✔
226
        mod = last(mods)
36✔
227
        if name_being_accessed isa QuoteNode
36✔
228
            name_being_accessed = name_being_accessed.value
36✔
229
        end
230
        name_being_accessed isa Symbol || return
36✔
231
        owner = try
36✔
232
            which(mod, name_being_accessed)
36✔
233
        catch
234
            return
×
235
        end
236
        # if `owner` is a submodule of `mod`, then don't warn. E.g. the name `parse` is present in the module `JSON`
237
        # but is owned by `JSON.Parser`; we don't warn if it is accessed as `JSON.parse`.
238
        has_ancestor(owner, mod) && return
61✔
239
        # Don't warn if the name is public in the module we are accessing it
240
        Base.ispublic(mod, name_being_accessed) && return
12✔
241
        # Don't warn if accessing names defined in Core from Base if they are present in Base (e.g. `Base.throw`).
242
        mod === Base && Base.ispublic(Core, name_being_accessed) && return
11✔
243
        push!(warnings, (; outer_mod, mod, owner, name_being_accessed))
11✔
244
        # no recursion
245
        return
11✔
246
    elseif Meta.isexpr(ast, :(=), 2)
586✔
247
        lhs, rhs = ast.args
47✔
248
        # any symbols we find on the LHS we will count as local. This can potentially be overzealous,
249
        # but we want to avoid false positives (unnecessary warnings) more than false negatives.
250
        add_locals!(locals, lhs)
47✔
251
        # we'll recurse into the RHS only
252
        return collect_names_to_warn!(warnings, locals, current_module, rhs)
47✔
253
    elseif Meta.isexpr(ast, :function) && length(ast.args) >= 1
539✔
254

255
        if Meta.isexpr(ast.args[1], :call, 2)
3✔
256
            func_name, func_args = ast.args[1].args
2✔
257
            # here we have a function definition and are inspecting it's arguments for local variables.
258
            # we will error on the conservative side by adding all symbols we find (regardless if they are local variables or possibly-global default values)
259
            add_locals!(locals, func_args)
2✔
260
        end
261
        # fall through to general recursion
262
    end
263

264
    for arg in ast.args
539✔
265
        collect_names_to_warn!(warnings, locals, current_module, arg)
1,000✔
266
    end
1,000✔
267

268
    return nothing
539✔
269
end
270

271
function collect_qualified_access_warnings(current_mod, ast)
120✔
272
    warnings = Set()
120✔
273
    locals = Set{Symbol}()
120✔
274
    collect_names_to_warn!(warnings, locals, current_mod, ast)
120✔
275
    filter!(warnings) do (; outer_mod)
120✔
276
        nameof(outer_mod) ∉ locals
11✔
277
    end
278
    return warnings
120✔
279
end
280

281
function warn_on_non_owning_accesses(current_mod, ast)
108✔
282
    warnings = collect_qualified_access_warnings(current_mod, ast)
108✔
283
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
216✔
284
        print_qualified_access_warning(mod, owner, name_being_accessed)
2✔
285
    end
4✔
286
    return ast
108✔
287
end
288
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
106✔
289

290
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
291

292
# Allows an external package to add hooks into the code loading.
293
# The hook should take a Vector{Symbol} of package names and
294
# return true if all packages could be installed, false if not
295
# to e.g. install packages on demand
296
const install_packages_hooks = Any[]
297

298
# N.B.: Any functions starting with __repl_entry cut off backtraces when printing in the REPL.
299
# We need to do this for both the actual eval and macroexpand, since the latter can cause custom macro
300
# code to run (and error).
301
__repl_entry_lower_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Csize_t}) =
211✔
302
    Core._lower(ast, mod, toplevel_file[], toplevel_line[])[1]
303
__repl_entry_eval_expanded_with_loc(mod::Module, @nospecialize(ast), toplevel_file::Ref{Ptr{UInt8}}, toplevel_line::Ref{Csize_t}) =
210✔
304
    ccall(:jl_toplevel_eval_flex, Any, (Any, Any, Cint, Cint, Ptr{Ptr{UInt8}}, Ptr{Csize_t}), mod, ast, 1, 1, toplevel_file, toplevel_line)
305

306
function toplevel_eval_with_hooks(mod::Module, @nospecialize(ast), toplevel_file=Ref{Ptr{UInt8}}(Base.unsafe_convert(Ptr{UInt8}, :REPL)), toplevel_line=Ref{Csize_t}(1))
321✔
307
    if !isexpr(ast, :toplevel)
427✔
308
        ast = invokelatest(__repl_entry_lower_with_loc, mod, ast, toplevel_file, toplevel_line)
211✔
309
        check_for_missing_packages_and_run_hooks(ast)
211✔
310
        return invokelatest(__repl_entry_eval_expanded_with_loc, mod, ast, toplevel_file, toplevel_line)
210✔
311
    end
312
    local value=nothing
110✔
313
    for i = 1:length(ast.args)
112✔
314
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
215✔
315
    end
314✔
316
    return value
104✔
317
end
318

319
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
106✔
320
    lasterr = nothing
106✔
321
    Base.sigatomic_begin()
106✔
322
    while true
110✔
323
        try
110✔
324
            Base.sigatomic_end()
110✔
325
            if lasterr !== nothing
110✔
326
                put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
4✔
327
            else
328
                backend.in_eval = true
106✔
329
                for xf in backend.ast_transforms
106✔
330
                    ast = Base.invokelatest(xf, ast)
230✔
331
                end
230✔
332
                value = toplevel_eval_with_hooks(mod, ast)
106✔
333
                backend.in_eval = false
101✔
334
                setglobal!(Base.MainInclude, :ans, value)
101✔
335
                put!(backend.response_channel, Pair{Any, Bool}(value, false))
101✔
336
            end
337
            break
109✔
338
        catch err
339
            if lasterr !== nothing
4✔
340
                println("SYSTEM ERROR: Failed to report error to REPL frontend")
×
341
                println(err)
×
342
            end
343
            lasterr = current_exceptions()
4✔
344
        end
345
    end
4✔
346
    Base.sigatomic_end()
105✔
347
    nothing
105✔
348
end
349

350
function check_for_missing_packages_and_run_hooks(ast)
211✔
351
    isa(ast, Expr) || return
317✔
352
    mods = modules_to_be_loaded(ast)
105✔
353
    filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
106✔
354
    if !isempty(mods)
105✔
355
        isempty(install_packages_hooks) && load_pkg()
1✔
356
        for f in install_packages_hooks
1✔
357
            Base.invokelatest(f, mods) && return
1✔
358
        end
×
359
    end
360
end
361

362
function _modules_to_be_loaded!(ast::Expr, mods::Vector{Symbol})
792✔
363
    function add!(ctx)
816✔
364
        if ctx.head == :as
24✔
365
            ctx = ctx.args[1]
1✔
366
        end
367
        if ctx.args[1] != :. # don't include local import `import .Foo`
48✔
368
            push!(mods, ctx.args[1])
21✔
369
        end
370
    end
371
    ast.head === :quote && return mods # don't search if it's not going to be run during this eval
792✔
372
    if ast.head == :call
792✔
373
        if length(ast.args) == 5 && ast.args[1] === GlobalRef(Base, :_eval_import)
423✔
374
            ctx = ast.args[4]
8✔
375
            if ctx isa QuoteNode # i.e. `Foo: bar`
8✔
376
                ctx = ctx.value
4✔
377
            else
378
                ctx = ast.args[5].value
4✔
379
            end
380
            add!(ctx)
8✔
381
        elseif length(ast.args) == 3 && ast.args[1] == GlobalRef(Base, :_eval_using)
415✔
382
            add!(ast.args[3].value)
16✔
383
        end
384
    end
385
    if ast.head !== :thunk
792✔
386
        for arg in ast.args
663✔
387
            if isexpr(arg, (:block, :if))
1,567✔
388
                _modules_to_be_loaded!(arg, mods)
4✔
389
            end
390
        end
1,567✔
391
    else
392
        code = ast.args[1]
129✔
393
        for arg in code.code
129✔
394
            isa(arg, Expr) || continue
1,153✔
395
            _modules_to_be_loaded!(arg, mods)
660✔
396
        end
1,153✔
397
    end
398
end
399

400
function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
401
    _modules_to_be_loaded!(ast, mods)
256✔
402
    filter!(mod::Symbol -> !in(mod, (:Base, :Main, :Core)), mods) # Exclude special non-package modules
149✔
403
    return unique(mods)
128✔
404
end
405

406
"""
407
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
408

409
    Starts loop for REPL backend
410
    Returns a REPLBackend with backend_task assigned
411

412
    Deprecated since sync / async behavior cannot be selected
413
"""
414
function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
×
415
                            ; get_module::Function = ()->Main)
416
    # Maintain legacy behavior of asynchronous backend
417
    backend = REPLBackend(repl_channel, response_channel, false)
×
418
    # Assignment will be made twice, but will be immediately available
419
    backend.backend_task = @async start_repl_backend(backend; get_module)
×
420
    return backend
×
421
end
422

423
"""
424
    start_repl_backend(backend::REPLBackend)
425

426
    Call directly to run backend loop on current Task.
427
    Use @async for run backend on new Task.
428

429
    Does not return backend until loop is finished.
430
"""
431
function start_repl_backend(backend::REPLBackend,  @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
56✔
432
    backend.backend_task = Base.current_task()
25✔
433
    consumer(backend)
25✔
434
    repl_backend_loop(backend, get_module)
25✔
435
    return backend
24✔
436
end
437

438
function repl_backend_loop(backend::REPLBackend, get_module::Function)
25✔
439
    # include looks at this to determine the relative include path
440
    # nothing means cwd
441
    while true
189✔
442
        tls = task_local_storage()
189✔
443
        tls[:SOURCE_PATH] = nothing
189✔
444
        ast_or_func, show_value = take!(backend.repl_channel)
189✔
445
        if show_value == -1
189✔
446
            # exit flag
447
            break
24✔
448
        end
449
        if show_value == 2 # 2 indicates a function to be called
165✔
450
            f = ast_or_func
59✔
451
            try
59✔
452
                ret = f()
59✔
453
                put!(backend.response_channel, Pair{Any, Bool}(ret, false))
58✔
454
            catch
455
                put!(backend.response_channel, Pair{Any, Bool}(current_exceptions(), true))
1✔
456
            end
457
        else
458
            ast = ast_or_func
106✔
459
            eval_user_input(ast, backend, get_module())
106✔
460
        end
461
    end
164✔
462
    return nothing
24✔
463
end
464

465
SHOW_MAXIMUM_BYTES::Int = 1_048_576
466

467
# Limit printing during REPL display
468
mutable struct LimitIO{IO_t <: IO} <: IO
469
    io::IO_t
70✔
470
    maxbytes::Int
471
    n::Int # max bytes to write
472
end
473
LimitIO(io::IO, maxbytes) = LimitIO(io, maxbytes, 0)
70✔
474

475
struct LimitIOException <: Exception
476
    maxbytes::Int
6✔
477
end
478

479
function Base.showerror(io::IO, e::LimitIOException)
×
480
    print(io, "$LimitIOException: aborted printing after attempting to print more than $(Base.format_bytes(e.maxbytes)) within a `LimitIO`.")
×
481
end
482

483
Base.displaysize(io::LimitIO) = _displaysize(io.io)
28✔
484

485
function Base.write(io::LimitIO, v::UInt8)
151✔
486
    io.n > io.maxbytes && throw(LimitIOException(io.maxbytes))
1,670✔
487
    n_bytes = write(io.io, v)
3,185✔
488
    io.n += n_bytes
1,668✔
489
    return n_bytes
1,668✔
490
end
491

492
# Semantically, we only need to override `Base.write`, but we also
493
# override `unsafe_write` for performance.
494
function Base.unsafe_write(limiter::LimitIO, p::Ptr{UInt8}, nb::UInt)
495
    # already exceeded? throw
496
    limiter.n > limiter.maxbytes && throw(LimitIOException(limiter.maxbytes))
266✔
497
    remaining = limiter.maxbytes - limiter.n # >= 0
266✔
498

499
    # Not enough bytes left; we will print up to the limit, then throw
500
    if remaining < nb
266✔
501
        if remaining > 0
2✔
502
            Base.unsafe_write(limiter.io, p, remaining)
1✔
503
        end
504
        throw(LimitIOException(limiter.maxbytes))
2✔
505
    end
506

507
    # We won't hit the limit so we'll write the full `nb` bytes
508
    bytes_written = Base.unsafe_write(limiter.io, p, nb)::Union{Int,UInt}
264✔
509
    limiter.n += bytes_written
264✔
510
    return bytes_written
264✔
511
end
512

513
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
514
    repl::Repl
34✔
515
end
516

517
function show_limited(io::IO, mime::MIME, x)
67✔
518
    try
67✔
519
        # We wrap in a LimitIO to limit the amount of printing.
520
        # We unpack `IOContext`s, since we will pass the properties on the outside.
521
        inner = io isa IOContext ? io.io : io
67✔
522
        wrapped_limiter = IOContext(LimitIO(inner, SHOW_MAXIMUM_BYTES), io)
67✔
523
        # `show_repl` to allow the hook with special syntax highlighting
524
        show_repl(wrapped_limiter, mime, x)
67✔
525
    catch e
526
        e isa LimitIOException || rethrow()
5✔
527
        printstyled(io, """…[printing stopped after displaying $(Base.format_bytes(e.maxbytes)); call `show(stdout, MIME"text/plain"(), ans)` to print without truncation]"""; color=:light_yellow, bold=true)
3✔
528
    end
529
end
530

531
function display(d::REPLDisplay, mime::MIME"text/plain", x)
62✔
532
    x = Ref{Any}(x)
62✔
533
    with_repl_linfo(d.repl) do io
62✔
534
        io = IOContext(io, :limit => true, :module => Base.active_module(d)::Module)
121✔
535
        if d.repl isa LineEditREPL
62✔
536
            mistate = d.repl.mistate
59✔
537
            mode = LineEdit.mode(mistate)
59✔
538
            if mode isa LineEdit.Prompt
59✔
539
                LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
236✔
540
            end
541
        end
542
        get(io, :color, false)::Bool && write(io, answer_color(d.repl))
68✔
543
        if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
62✔
544
            # this can override the :limit property set initially
545
            io = foldl(IOContext, d.repl.options.iocontext, init=io)
59✔
546
        end
547
        show_limited(io, mime, x[])
62✔
548
        println(io)
61✔
549
    end
550
    return nothing
61✔
551
end
552

553
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
62✔
554

555
show_repl(io::IO, mime::MIME"text/plain", x) = show(io, mime, x)
65✔
556

557
function show_repl(io::IO, mime::MIME"text/plain", c::AbstractChar)
4✔
558
    show(io, mime, c) # Call the original Base.show
4✔
559
    # Check for LaTeX/emoji alias and print if found and using symbol_latex which is used in help?> mode
560
    latex = symbol_latex(string(c))
4✔
561
    if !isempty(latex)
4✔
562
        print(io, ", input as ")
2✔
563
        printstyled(io, latex, "<tab>"; color=:cyan)
2✔
564
    end
565
end
566

567
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
2✔
568
    print(io, JuliaSyntaxHighlighting.highlight(
569
        sprint(show, ex, context=IOContext(io, :color => false))))
570

571
function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
102✔
572
    repl.waserror = response[2]
102✔
573
    with_repl_linfo(repl) do io
204✔
574
        io = IOContext(io, :module => Base.active_module(repl)::Module)
201✔
575
        print_response(io, response, backend(repl), show_value, have_color, specialdisplay(repl))
102✔
576
    end
577
    return nothing
102✔
578
end
579

580
# N.B.: Any functions starting with __repl_entry cut off backtraces when printing in the REPL.
581
__repl_entry_display(val) = Base.invokelatest(display, val)
48✔
582
__repl_entry_display(specialdisplay::Union{AbstractDisplay,Nothing}, val) = Base.invokelatest(display, specialdisplay, val)
14✔
583

584
function __repl_entry_display_error(errio::IO, @nospecialize errval)
8✔
585
    # this will be set to true if types in the stacktrace are truncated
586
    limitflag = Ref(false)
8✔
587
    errio = IOContext(errio, :stacktrace_types_limited => limitflag)
8✔
588
    Base.invokelatest(Base.display_error, errio, errval)
8✔
589
    if limitflag[]
6✔
590
        print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
2✔
591
        println(errio)
2✔
592
    end
593
    return nothing
6✔
594
end
595

596
function print_response(errio::IO, response, backend::Union{REPLBackendRef,Nothing}, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
103✔
597
    Base.sigatomic_begin()
103✔
598
    val, iserr = response
103✔
599
    if !iserr
103✔
600
        # display result
601
        try
97✔
602
            if val !== nothing && show_value
97✔
603
                Base.sigatomic_end() # allow display to be interrupted
62✔
604
                val2, iserr = if specialdisplay === nothing
62✔
605
                    # display calls may require being run on the main thread
606
                    call_on_backend(backend) do
93✔
607
                        __repl_entry_display(val)
48✔
608
                    end
609
                else
610
                    call_on_backend(backend) do
76✔
611
                        __repl_entry_display(specialdisplay, val)
14✔
612
                    end
613
                end
614
                Base.sigatomic_begin()
62✔
615
                if iserr
62✔
616
                    println(errio)
1✔
617
                    println(errio, "Error showing value of type ", typeof(val), ":")
1✔
618
                    val = val2
1✔
619
                end
620
            end
621
        catch ex
622
            println(errio)
×
623
            println(errio, "SYSTEM (REPL): showing a value caused an error")
×
624
            val = current_exceptions()
×
625
            iserr = true
×
626
        end
627
    end
628
    if iserr
103✔
629
        # print error
630
        iserr = false
7✔
631
        while true
8✔
632
            try
8✔
633
                Base.sigatomic_end() # allow stacktrace printing to be interrupted
8✔
634
                val = Base.scrub_repl_backtrace(val)
8✔
635
                Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
14✔
636
                __repl_entry_display_error(errio, val)
8✔
637
                break
8✔
638
            catch ex
639
                println(errio) # an error during printing is likely to leave us mid-line
2✔
640
                if !iserr
2✔
641
                    println(errio, "SYSTEM (REPL): showing an error caused an error")
1✔
642
                    val = current_exceptions()
1✔
643
                    iserr = true
1✔
644
                else
645
                    println(errio, "SYSTEM (REPL): caught exception of type ", typeof(ex).name.name,
1✔
646
                        " while trying to print an exception; giving up")
647
                    break
2✔
648
                end
649
            end
650
        end
1✔
651
    end
652
    Base.sigatomic_end()
103✔
653
    nothing
103✔
654
end
655

656

657

658
"""
659
    run_repl(repl::AbstractREPL)
660
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
661

662
    Main function to start the REPL
663

664
    consumer is an optional function that takes a REPLBackend as an argument
665
"""
666
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
90✔
667
    backend_ref = REPLBackendRef(backend)
23✔
668
    cleanup = @task try
45✔
669
            destroy(backend_ref, t)
22✔
670
        catch e
UNCOV
671
            Core.print(Core.stderr, "\nINTERNAL ERROR: ")
×
UNCOV
672
            Core.println(Core.stderr, e)
×
UNCOV
673
            Core.println(Core.stderr, catch_backtrace())
×
674
        end
675
    get_module = () -> Base.active_module(repl)
125✔
676
    if backend_on_current_task
23✔
677
        t = @async run_frontend(repl, backend_ref)
46✔
678
        errormonitor(t)
23✔
679
        Base._wait2(t, cleanup)
23✔
680
        start_repl_backend(backend, consumer; get_module)
23✔
681
    else
682
        t = @async start_repl_backend(backend, consumer; get_module)
×
683
        errormonitor(t)
×
684
        Base._wait2(t, cleanup)
×
685
        run_frontend(repl, backend_ref)
×
686
    end
687
    return backend
22✔
688
end
689

690
## BasicREPL ##
691

692
mutable struct BasicREPL <: AbstractREPL
693
    terminal::TextTerminal
694
    waserror::Bool
695
    frontend_task::Task
696
    BasicREPL(t) = new(t, false)
3✔
697
end
698

699
outstream(r::BasicREPL) = r.terminal
6✔
700
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
701

702
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
3✔
703
    repl.frontend_task = current_task()
3✔
704
    d = REPLDisplay(repl)
3✔
705
    dopushdisplay = !in(d,Base.Multimedia.displays)
6✔
706
    dopushdisplay && pushdisplay(d)
3✔
707
    hit_eof = false
3✔
708
    while true
6✔
709
        Base.reseteof(repl.terminal)
6✔
710
        write(repl.terminal, JULIA_PROMPT)
6✔
711
        line = ""
6✔
712
        ast = nothing
6✔
713
        interrupted = false
6✔
714
        while true
6✔
715
            try
6✔
716
                line *= readline(repl.terminal, keep=true)
6✔
717
            catch e
718
                if isa(e,InterruptException)
×
719
                    try # raise the debugger if present
×
720
                        ccall(:jl_raise_debugger, Int, ())
×
721
                    catch
×
722
                    end
723
                    line = ""
×
724
                    interrupted = true
×
725
                    break
×
726
                elseif isa(e,EOFError)
×
727
                    hit_eof = true
×
728
                    break
×
729
                else
730
                    rethrow()
×
731
                end
732
            end
733
            ast = Base.parse_input_line(line)
12✔
734
            (isa(ast,Expr) && ast.head === :incomplete) || break
12✔
735
        end
×
736
        if !isempty(line)
6✔
737
            response = eval_on_backend(ast, backend)
4✔
738
            print_response(repl, response, !ends_with_semicolon(line), false)
3✔
739
        end
740
        write(repl.terminal, '\n')
5✔
741
        ((!interrupted && isempty(line)) || hit_eof) && break
8✔
742
    end
3✔
743
    # terminate backend
744
    put!(backend.repl_channel, (nothing, -1))
2✔
745
    dopushdisplay && popdisplay(d)
2✔
746
    nothing
2✔
747
end
748

749
## LineEditREPL ##
750

751
mutable struct LineEditREPL <: AbstractREPL
752
    t::TextTerminal
753
    hascolor::Bool
754
    prompt_color::String
755
    input_color::String
756
    answer_color::String
757
    shell_color::String
758
    help_color::String
759
    pkg_color::String
760
    history_file::Bool
761
    in_shell::Bool
762
    in_help::Bool
763
    envcolors::Bool
764
    waserror::Bool
765
    specialdisplay::Union{Nothing,AbstractDisplay}
766
    options::Options
767
    mistate::Union{MIState,Nothing}
768
    last_shown_line_infos::Vector{Tuple{String,Int}}
769
    interface::ModalInterface
770
    backendref::REPLBackendRef
771
    frontend_task::Task
772
    function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
27✔
773
        opts = Options()
27✔
774
        opts.hascolor = hascolor
27✔
775
        if !hascolor
27✔
776
            opts.beep_colors = [""]
×
777
        end
778
        new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
27✔
779
            in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
780
    end
781
end
782
outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
168✔
783
specialdisplay(r::LineEditREPL) = r.specialdisplay
99✔
784
specialdisplay(r::AbstractREPL) = nothing
3✔
785
terminal(r::LineEditREPL) = r.t
154✔
786
hascolor(r::LineEditREPL) = r.hascolor
199✔
787

788
LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
54✔
789
    LineEditREPL(t, hascolor,
790
        hascolor ? Base.text_colors[:green] : "",
791
        hascolor ? Base.input_color() : "",
792
        hascolor ? Base.answer_color() : "",
793
        hascolor ? Base.text_colors[:red] : "",
794
        hascolor ? Base.text_colors[:yellow] : "",
795
        hascolor ? Base.text_colors[:blue] : "",
796
        false, false, false, envcolors
797
    )
798

799
mutable struct REPLCompletionProvider <: CompletionProvider
800
    modifiers::LineEdit.Modifiers
24✔
801
end
802
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
24✔
803

804
mutable struct ShellCompletionProvider <: CompletionProvider end
24✔
805
struct LatexCompletions <: CompletionProvider end
24✔
806

807
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
5,078✔
808
Base.active_module(::AbstractREPL) = Main
12✔
809
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
121✔
810

811
setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
×
812

813
setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
×
814

815
"""
816
    activate(mod::Module=Main)
817

818
Set `mod` as the default contextual module in the REPL,
819
both for evaluating expressions and printing them.
820
"""
821
function activate(mod::Module=Main; interactive_utils::Bool=true)
×
822
    mistate = (Base.active_repl::LineEditREPL).mistate
×
823
    mistate === nothing && return nothing
×
824
    mistate.active_module = mod
×
825
    interactive_utils && Base.load_InteractiveUtils(mod)
×
826
    return nothing
×
827
end
828

829
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
32✔
830

831
# Convert inclusive-inclusive 1-based char indexing to inclusive-exclusive byte Region.
832
to_region(s, r) = first(r)-1 => (length(r) > 0 ? nextind(s, last(r))-1 : first(r)-1)
26✔
833

834
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
24✔
835
    full = LineEdit.input_string(s)
12✔
836
    ret, range, should_complete = completions(full, thisind(full, position(s)), mod, c.modifiers.shift, hint)
24✔
837
    range = to_region(full, range)
23✔
838
    c.modifiers = LineEdit.Modifiers()
12✔
839
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
12✔
840
end
841

842
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
4✔
843
    full = LineEdit.input_string(s)
2✔
844
    ret, range, should_complete = shell_completions(full, thisind(full, position(s)), hint)
4✔
845
    range = to_region(full, range)
3✔
846
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
2✔
847
end
848

849
function complete_line(c::LatexCompletions, s; hint::Bool=false)
×
850
    full = LineEdit.input_string(s)::String
×
851
    ret, range, should_complete = bslash_completions(full, thisind(full, position(s)), hint)[2]
×
852
    range = to_region(full, range)
×
853
    return unique!(LineEdit.NamedCompletion[named_completion(x) for x in ret]), range, should_complete
×
854
end
855

856
with_repl_linfo(f, repl) = f(outstream(repl))
6✔
857
function with_repl_linfo(f, repl::LineEditREPL)
158✔
858
    linfos = Tuple{String,Int}[]
158✔
859
    io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
158✔
860
    f(io)
158✔
861
    if !isempty(linfos)
157✔
862
        repl.last_shown_line_infos = linfos
6✔
863
    end
864
    nothing
157✔
865
end
866

867
mutable struct REPLHistoryProvider <: HistoryProvider
868
    history::Vector{String}
29✔
869
    file_path::String
870
    history_file::Union{Nothing,IO}
871
    start_idx::Int
872
    cur_idx::Int
873
    last_idx::Int
874
    last_buffer::IOBuffer
875
    last_mode::Union{Nothing,Prompt}
876
    mode_mapping::Dict{Symbol,Prompt}
877
    modes::Vector{Symbol}
878
end
879
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
29✔
880
    REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
881
                        nothing, mode_mapping, UInt8[])
882

883
invalid_history_message(path::String) = """
×
884
Invalid history file ($path) format:
885
If you have a history file left over from an older version of Julia,
886
try renaming or deleting it.
887
Invalid character: """
888

889
munged_history_message(path::String) = """
×
890
Invalid history file ($path) format:
891
An editor may have converted tabs to spaces at line """
892

893
function hist_open_file(hp::REPLHistoryProvider)
894
    f = open(hp.file_path, read=true, write=true, create=true)
4✔
895
    hp.history_file = f
4✔
896
    seekend(f)
4✔
897
end
898

899
function hist_from_file(hp::REPLHistoryProvider, path::String)
8✔
900
    getline(lines, i) = i > length(lines) ? "" : lines[i]
284✔
901
    file_lines = readlines(path)
8✔
902
    countlines = 0
8✔
903
    while true
42✔
904
        # First parse the metadata that starts with '#' in particular the REPL mode
905
        countlines += 1
42✔
906
        line = getline(file_lines, countlines)
76✔
907
        mode = :julia
42✔
908
        isempty(line) && break
42✔
909
        line[1] != '#' &&
68✔
910
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
911
        while !isempty(line)
102✔
912
            startswith(line, '#') || break
204✔
913
            if startswith(line, "# mode: ")
68✔
914
                mode = Symbol(SubString(line, 9))
68✔
915
            end
916
            countlines += 1
68✔
917
            line = getline(file_lines, countlines)
136✔
918
        end
68✔
919
        isempty(line) && break
34✔
920

921
        # Now parse the code for the current REPL mode
922
        line[1] == ' '  &&
68✔
923
            error(munged_history_message(path), countlines)
924
        line[1] != '\t' &&
68✔
925
            error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
926
        lines = String[]
34✔
927
        while !isempty(line)
34✔
928
            push!(lines, chomp(SubString(line, 2)))
68✔
929
            next_line = getline(file_lines, countlines+1)
64✔
930
            isempty(next_line) && break
34✔
931
            first(next_line) == ' '  && error(munged_history_message(path), countlines)
60✔
932
            # A line not starting with a tab means we are done with code for this entry
933
            first(next_line) != '\t' && break
60✔
934
            countlines += 1
×
935
            line = getline(file_lines, countlines)
×
936
        end
×
937
        push!(hp.modes, mode)
34✔
938
        push!(hp.history, join(lines, '\n'))
34✔
939
    end
34✔
940
    hp.start_idx = length(hp.history)
8✔
941
    return hp
8✔
942
end
943

944
function add_history(hist::REPLHistoryProvider, s::PromptState)
117✔
945
    str = rstrip(takestring!(copy(s.input_buffer)))
117✔
946
    isempty(strip(str)) && return
117✔
947
    mode = mode_idx(hist, LineEdit.mode(s))
99✔
948
    !isempty(hist.history) &&
99✔
949
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
950
    push!(hist.modes, mode)
93✔
951
    push!(hist.history, str)
93✔
952
    hist.history_file === nothing && return
93✔
953
    entry = """
16✔
954
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
955
    # mode: $mode
956
    $(replace(str, r"^"ms => "\t"))
957
    """
958
    try
16✔
959
        seekend(hist.history_file)
16✔
960
    catch err
961
        (err isa SystemError) || rethrow()
×
962
        # File handle might get stale after a while, especially under network file systems
963
        # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
964
        hist_open_file(hist)
×
965
    end
966
    if isfile(hist.file_path)
16✔
967
        FileWatching.mkpidlock(hist.file_path  * ".pid", stale_age=3) do
16✔
968
            print(hist.history_file, entry)
32✔
969
            flush(hist.history_file)
16✔
970
        end
971
    else # handle eg devnull
972
        print(hist.history_file, entry)
×
973
        flush(hist.history_file)
×
974
    end
975
    nothing
16✔
976
end
977

978
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
96✔
979
    max_idx = length(hist.history) + 1
136✔
980
    @assert 1 <= hist.cur_idx <= max_idx
96✔
981
    (1 <= idx <= max_idx) || return :none
98✔
982
    idx != hist.cur_idx || return :none
94✔
983

984
    # save the current line
985
    if save_idx == max_idx
94✔
986
        hist.last_mode = LineEdit.mode(s)
31✔
987
        hist.last_buffer = copy(LineEdit.buffer(s))
46✔
988
    else
989
        hist.history[save_idx] = LineEdit.input_string(s)
63✔
990
        hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
63✔
991
    end
992

993
    # load the saved line
994
    if idx == max_idx
94✔
995
        last_buffer = hist.last_buffer
10✔
996
        LineEdit.transition(s, hist.last_mode) do
14✔
997
            LineEdit.replace_line(s, last_buffer)
10✔
998
        end
999
        hist.last_mode = nothing
10✔
1000
        hist.last_buffer = IOBuffer()
10✔
1001
    else
1002
        if haskey(hist.mode_mapping, hist.modes[idx])
168✔
1003
            LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
68✔
1004
                LineEdit.replace_line(s, hist.history[idx])
68✔
1005
            end
1006
        else
1007
            return :skip
16✔
1008
        end
1009
    end
1010
    hist.cur_idx = idx
78✔
1011

1012
    return :ok
78✔
1013
end
1014

1015
# REPL History can also transitions modes
1016
function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
27✔
1017
    if 1 <= hist.cur_idx <= length(hist.modes)
27✔
1018
        return hist.mode_mapping[hist.modes[hist.cur_idx]]
23✔
1019
    end
1020
    return nothing
4✔
1021
end
1022

1023
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
34✔
1024
                      num::Int=1, save_idx::Int = hist.cur_idx)
1025
    num <= 0 && return history_next(s, hist, -num, save_idx)
58✔
1026
    hist.last_idx = -1
32✔
1027
    m = history_move(s, hist, hist.cur_idx-num, save_idx)
32✔
1028
    if m === :ok
32✔
1029
        LineEdit.move_input_start(s)
48✔
1030
        LineEdit.reset_key_repeats(s) do
24✔
1031
            LineEdit.move_line_end(s)
24✔
1032
        end
1033
        return LineEdit.refresh_line(s)
24✔
1034
    elseif m === :skip
8✔
1035
        return history_prev(s, hist, num+1, save_idx)
8✔
1036
    else
1037
        return Terminals.beep(s)
×
1038
    end
1039
end
1040

1041
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
26✔
1042
                      num::Int=1, save_idx::Int = hist.cur_idx)
1043
    if num == 0
44✔
1044
        Terminals.beep(s)
×
1045
        return
×
1046
    end
1047
    num < 0 && return history_prev(s, hist, -num, save_idx)
26✔
1048
    cur_idx = hist.cur_idx
24✔
1049
    max_idx = length(hist.history) + 1
24✔
1050
    if cur_idx == max_idx && 0 < hist.last_idx
24✔
1051
        # issue #6312
1052
        cur_idx = hist.last_idx
×
1053
        hist.last_idx = -1
×
1054
    end
1055
    m = history_move(s, hist, cur_idx+num, save_idx)
24✔
1056
    if m === :ok
24✔
1057
        LineEdit.move_input_end(s)
16✔
1058
        return LineEdit.refresh_line(s)
16✔
1059
    elseif m === :skip
8✔
1060
        return history_next(s, hist, num+1, save_idx)
6✔
1061
    else
1062
        return Terminals.beep(s)
2✔
1063
    end
1064
end
1065

1066
history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
6✔
1067
    history_prev(s, hist, hist.cur_idx - 1 -
1068
                 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
1069

1070
history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
4✔
1071
    history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
1072

1073
function history_move_prefix(s::LineEdit.PrefixSearchState,
38✔
1074
                             hist::REPLHistoryProvider,
1075
                             prefix::AbstractString,
1076
                             backwards::Bool,
1077
                             cur_idx::Int = hist.cur_idx)
1078
    cur_response = takestring!(copy(LineEdit.buffer(s)))
74✔
1079
    # when searching forward, start at last_idx
1080
    if !backwards && hist.last_idx > 0
38✔
1081
        cur_idx = hist.last_idx
1✔
1082
    end
1083
    hist.last_idx = -1
38✔
1084
    max_idx = length(hist.history)+1
38✔
1085
    idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
65✔
1086
    for idx in idxs
38✔
1087
        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✔
1088
            m = history_move(s, hist, idx)
36✔
1089
            if m === :ok
36✔
1090
                if idx == max_idx
34✔
1091
                    # on resuming the in-progress edit, leave the cursor where the user last had it
1092
                elseif isempty(prefix)
30✔
1093
                    # on empty prefix search, move cursor to the end
1094
                    LineEdit.move_input_end(s)
14✔
1095
                else
1096
                    # otherwise, keep cursor at the prefix position as a visual cue
1097
                    seek(LineEdit.buffer(s), sizeof(prefix))
16✔
1098
                end
1099
                LineEdit.refresh_line(s)
34✔
1100
                return :ok
34✔
1101
            elseif m === :skip
2✔
1102
                return history_move_prefix(s,hist,prefix,backwards,idx)
2✔
1103
            end
1104
        end
1105
    end
124✔
1106
    Terminals.beep(s)
2✔
1107
    nothing
2✔
1108
end
1109
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
5✔
1110
    history_move_prefix(s, hist, prefix, false)
1111
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
31✔
1112
    history_move_prefix(s, hist, prefix, true)
1113

1114
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
28✔
1115
                        backwards::Bool=false, skip_current::Bool=false)
1116

1117
    qpos = position(query_buffer)
28✔
1118
    qpos > 0 || return true
28✔
1119
    searchdata = beforecursor(query_buffer)
28✔
1120
    response_str = takestring!(copy(response_buffer))
28✔
1121

1122
    # Alright, first try to see if the current match still works
1123
    a = position(response_buffer) + 1 # position is zero-indexed
28✔
1124
    # FIXME: I'm pretty sure this is broken since it uses an index
1125
    # into the search data to index into the response string
1126
    b = a + sizeof(searchdata)
28✔
1127
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
48✔
1128
    b = min(lastindex(response_str), b) # ensure that b is valid
50✔
1129

1130
    searchstart = backwards ? b : a
28✔
1131
    if searchdata == response_str[a:b]
44✔
1132
        if skip_current
10✔
1133
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
4✔
1134
        else
1135
            return true
6✔
1136
        end
1137
    end
1138

1139
    # Start searching
1140
    # First the current response buffer
1141
    if 1 <= searchstart <= lastindex(response_str)
36✔
1142
        match = backwards ? findprev(searchdata, response_str, searchstart) :
14✔
1143
                            findnext(searchdata, response_str, searchstart)
1144
        if match !== nothing
14✔
1145
            seek(response_buffer, first(match) - 1)
12✔
1146
            return true
6✔
1147
        end
1148
    end
1149

1150
    # Now search all the other buffers
1151
    idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
32✔
1152
    for idx in idxs
16✔
1153
        h = hist.history[idx]
40✔
1154
        match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
80✔
1155
        if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
54✔
1156
            truncate(response_buffer, 0)
12✔
1157
            write(response_buffer, h)
12✔
1158
            seek(response_buffer, first(match) - 1)
24✔
1159
            hist.cur_idx = idx
12✔
1160
            return true
12✔
1161
        end
1162
    end
52✔
1163

1164
    return false
4✔
1165
end
1166

1167
function history_reset_state(hist::REPLHistoryProvider)
1168
    if hist.cur_idx != length(hist.history) + 1
271✔
1169
        hist.last_idx = hist.cur_idx
130✔
1170
        hist.cur_idx = length(hist.history) + 1
130✔
1171
    end
1172
    nothing
271✔
1173
end
1174
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
240✔
1175

1176
function return_callback(s)
96✔
1177
    ast = Base.parse_input_line(takestring!(copy(LineEdit.buffer(s))), depwarn=false)
96✔
1178
    return !(isa(ast, Expr) && ast.head === :incomplete)
96✔
1179
end
1180

1181
find_hist_file() = get(ENV, "JULIA_HISTORY",
4✔
1182
                       !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
1183
                       error("DEPOT_PATH is empty and ENV[\"JULIA_HISTORY\"] not set."))
1184

1185
backend(r::AbstractREPL) = hasproperty(r, :backendref) && isdefined(r, :backendref) ? r.backendref : nothing
200✔
1186

1187

1188
function eval_on_backend(ast, backend::REPLBackendRef)
102✔
1189
    put!(backend.repl_channel, (ast, 1)) # (f, show_value)
102✔
1190
    return take!(backend.response_channel) # (val, iserr)
102✔
1191
end
1192
function call_on_backend(f, backend::REPLBackendRef)
14✔
1193
    applicable(f) || error("internal error: f is not callable")
59✔
1194
    put!(backend.repl_channel, (f, 2)) # (f, show_value) 2 indicates function (rather than ast)
59✔
1195
    return take!(backend.response_channel) # (val, iserr)
59✔
1196
end
1197
# if no backend just eval (used by tests)
1198
eval_on_backend(ast, backend::Nothing) = error("no backend for eval ast")
×
1199
function call_on_backend(f, backend::Nothing)
3✔
1200
    try
3✔
1201
        ret = f()
3✔
1202
        return (ret, false) # (val, iserr)
3✔
1203
    catch
1204
        return (current_exceptions(), true)
×
1205
    end
1206
end
1207

1208

1209
function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
96✔
1210
    return function do_respond(s::MIState, buf, ok::Bool)
230✔
1211
        if !ok
134✔
1212
            return transition(s, :abort)
20✔
1213
        end
1214
        line = String(take!(buf)::Vector{UInt8})
213✔
1215
        if !isempty(line) || pass_empty
129✔
1216
            reset(repl)
99✔
1217
            local response
1218
            try
99✔
1219
                ast = Base.invokelatest(f, line)
99✔
1220
                response = eval_on_backend(ast, backend(repl))
196✔
1221
            catch
1222
                response = Pair{Any, Bool}(current_exceptions(), true)
1✔
1223
            end
1224
            hide_output = suppress_on_semicolon && ends_with_semicolon(line)
99✔
1225
            print_response(repl, response, !hide_output, hascolor(repl))
99✔
1226
        end
1227
        prepare_next(repl)
114✔
1228
        reset_state(s)
114✔
1229
        return s.current_mode.sticky ? true : transition(s, main)
114✔
1230
    end
1231
end
1232

1233
function reset(repl::LineEditREPL)
99✔
1234
    raw!(repl.t, false)
99✔
1235
    hascolor(repl) && print(repl.t, Base.text_colors[:normal])
99✔
1236
    nothing
99✔
1237
end
1238

1239
function prepare_next(repl::LineEditREPL)
114✔
1240
    println(terminal(repl))
114✔
1241
end
1242

1243
function mode_keymap(julia_prompt::Prompt)
1244
    AnyDict(
26✔
1245
    '\b' => function (s::MIState,o...)
7✔
1246
        if isempty(s) || position(LineEdit.buffer(s)) == 0
7✔
1247
            buf = copy(LineEdit.buffer(s))
7✔
1248
            transition(s, julia_prompt) do
7✔
1249
                LineEdit.state(s, julia_prompt).input_buffer = buf
7✔
1250
            end
1251
        else
UNCOV
1252
            LineEdit.edit_backspace(s)
×
1253
        end
1254
    end,
1255
    "^C" => function (s::MIState,o...)
1256
        LineEdit.move_input_end(s)
1257
        LineEdit.refresh_line(s)
1258
        print(LineEdit.terminal(s), "^C\n\n")
1259
        transition(s, julia_prompt)
1260
        transition(s, :reset)
1261
        LineEdit.refresh_line(s)
1262
    end)
1263
end
1264

1265
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
88✔
1266
repl_filename(repl, hp) = "REPL"
×
1267

1268
const JL_PROMPT_PASTE = Ref(true)
1269
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
×
1270

1271
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
1272
    function ()
2,336✔
1273
        mod = Base.active_module(repl)
4,566✔
1274
        prefix = mod == Main ? "" : string('(', mod, ") ")
2,314✔
1275
        pr = prompt isa String ? prompt : prompt()
2,287✔
1276
        prefix * pr
2,287✔
1277
    end
1278
end
1279

1280
setup_interface(
68✔
1281
    repl::LineEditREPL;
1282
    # those keyword arguments may be deprecated eventually in favor of the Options mechanism
1283
    hascolor::Bool = repl.options.hascolor,
1284
    extra_repl_keymap::Any = repl.options.extra_keymap
1285
) = setup_interface(repl, hascolor, extra_repl_keymap)
1286

1287

1288
# This non keyword method can be precompiled which is important
1289
function setup_interface(
24✔
1290
    repl::LineEditREPL,
1291
    hascolor::Bool,
1292
    extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
1293
)
1294
    # The precompile statement emitter has problem outputting valid syntax for the
1295
    # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
1296
    # This function is however important to precompile for REPL startup time, therefore,
1297
    # make the type Any and just assert that we have the correct type below.
1298
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
24✔
1299

1300
    ###
1301
    #
1302
    # This function returns the main interface that describes the REPL
1303
    # functionality, it is called internally by functions that setup a
1304
    # Terminal-based REPL frontend.
1305
    #
1306
    # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1307
    # for usage
1308
    #
1309
    ###
1310

1311
    ###
1312
    # We setup the interface in two stages.
1313
    # First, we set up all components (prompt,rsearch,shell,help)
1314
    # Second, we create keymaps with appropriate transitions between them
1315
    #   and assign them to the components
1316
    #
1317
    ###
1318

1319
    ############################### Stage I ################################
1320

1321
    # This will provide completions for REPL and help mode
1322
    replc = REPLCompletionProvider()
24✔
1323

1324
    # Set up the main Julia prompt
1325
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
48✔
1326
        # Copy colors from the prompt object
1327
        prompt_prefix = hascolor ? repl.prompt_color : "",
1328
        prompt_suffix = hascolor ?
1329
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1330
        repl = repl,
1331
        complete = replc,
1332
        on_enter = return_callback)
1333

1334
    # Setup help mode
1335
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
48✔
1336
        prompt_prefix = hascolor ? repl.help_color : "",
1337
        prompt_suffix = hascolor ?
1338
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1339
        repl = repl,
1340
        complete = replc,
1341
        # When we're done transform the entered line into a call to helpmode function
1342
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
2✔
1343
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1344

1345

1346
    # Set up shell mode
1347
    shell_mode = Prompt(SHELL_PROMPT;
48✔
1348
        prompt_prefix = hascolor ? repl.shell_color : "",
1349
        prompt_suffix = hascolor ?
1350
            (repl.envcolors ? Base.input_color : repl.input_color) : "",
1351
        repl = repl,
1352
        complete = ShellCompletionProvider(),
1353
        # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1354
        # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1355
        # special)
1356
        on_done = respond(repl, julia_prompt) do line
1357
            Expr(:call, :(Base.repl_cmd),
9✔
1358
                :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1359
                outstream(repl))
1360
        end,
1361
        sticky = true)
1362

1363
    # Set up dummy Pkg mode that will be replaced once Pkg is loaded
1364
    # use 6 dots to occupy the same space as the most likely "@v1.xx" env name
1365
    dummy_pkg_mode = Prompt(Pkg_promptf,
48✔
1366
        prompt_prefix = hascolor ? repl.pkg_color : "",
1367
        prompt_suffix = hascolor ?
1368
        (repl.envcolors ? Base.input_color : repl.input_color) : "",
1369
        repl = repl,
1370
        complete = LineEdit.EmptyCompletionProvider(),
1371
        on_done = respond(line->nothing, repl, julia_prompt),
1372
        on_enter = function (s::MIState)
1373
                # This is hit when the user tries to execute a command before the real Pkg mode has been
1374
                # switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
1375
                REPLExt = load_pkg()
1376
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1377
                    for mode in repl.interface.modes
1378
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1379
                            # pkg mode
1380
                            buf = copy(LineEdit.buffer(s))
1381
                            transition(s, mode) do
1382
                                LineEdit.state(s, mode).input_buffer = buf
1383
                            end
1384
                        end
1385
                    end
1386
                end
1387
                return true
1388
            end,
1389
        sticky = true)
1390

1391

1392
    ################################# Stage II #############################
1393

1394
    # Setup history
1395
    # We will have a unified history for all REPL modes
1396
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
24✔
1397
                                                 :shell => shell_mode,
1398
                                                 :help  => help_mode,
1399
                                                 :pkg  => dummy_pkg_mode))
1400
    if repl.history_file
24✔
1401
        try
4✔
1402
            hist_path = find_hist_file()
4✔
1403
            mkpath(dirname(hist_path))
4✔
1404
            hp.file_path = hist_path
4✔
1405
            hist_open_file(hp)
4✔
1406
            finalizer(replc) do replc
4✔
1407
                close(hp.history_file)
4✔
1408
            end
1409
            hist_from_file(hp, hist_path)
4✔
1410
        catch
1411
            # use REPL.hascolor to avoid using the local variable with the same name
1412
            print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
×
1413
            println(outstream(repl))
×
1414
            @info "Disabling history file for this session"
×
1415
            repl.history_file = false
×
1416
        end
1417
    end
1418
    history_reset_state(hp)
24✔
1419
    julia_prompt.hist = hp
24✔
1420
    shell_mode.hist = hp
24✔
1421
    help_mode.hist = hp
24✔
1422
    dummy_pkg_mode.hist = hp
24✔
1423

1424
    julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
112✔
1425

1426

1427
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
24✔
1428
    search_prompt.complete = LatexCompletions()
24✔
1429

1430
    shell_prompt_len = length(SHELL_PROMPT)
24✔
1431
    help_prompt_len = length(HELP_PROMPT)
24✔
1432
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
24✔
1433
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
24✔
1434

1435
    # Canonicalize user keymap input
1436
    if isa(extra_repl_keymap, Dict)
24✔
1437
        extra_repl_keymap = AnyDict[extra_repl_keymap]
×
1438
    end
1439

1440
    repl_keymap = AnyDict(
24✔
1441
        ';' => function (s::MIState,o...)
55✔
1442
            if isempty(s) || position(LineEdit.buffer(s)) == 0
103✔
1443
                buf = copy(LineEdit.buffer(s))
7✔
1444
                transition(s, shell_mode) do
7✔
1445
                    LineEdit.state(s, shell_mode).input_buffer = buf
7✔
1446
                end
1447
            else
1448
                edit_insert(s, ';')
48✔
1449
                LineEdit.check_show_hint(s)
48✔
1450
            end
1451
        end,
1452
        '?' => function (s::MIState,o...)
1✔
1453
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1✔
1454
                buf = copy(LineEdit.buffer(s))
1✔
1455
                transition(s, help_mode) do
1✔
1456
                    LineEdit.state(s, help_mode).input_buffer = buf
1✔
1457
                end
1458
            else
1459
                edit_insert(s, '?')
×
1460
                LineEdit.check_show_hint(s)
×
1461
            end
1462
        end,
1463
        ']' => function (s::MIState,o...)
3✔
1464
            if isempty(s) || position(LineEdit.buffer(s)) == 0
6✔
1465
                buf = copy(LineEdit.buffer(s))
×
1466
                transition(s, dummy_pkg_mode) do
×
1467
                    LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
1468
                end
1469
                # load Pkg on another thread if available so that typing in the dummy Pkg prompt
1470
                # isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
1471
                t_replswitch = Threads.@spawn begin
×
1472
                    REPLExt = load_pkg()
1473
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1474
                        put!(s.async_channel,
1475
                            function (s::MIState)
1476
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
1477
                                for mode in repl.interface.modes
1478
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1479
                                        buf = copy(LineEdit.buffer(s))
1480
                                        transition(s, mode) do
1481
                                            LineEdit.state(s, mode).input_buffer = buf
1482
                                        end
1483
                                        if !isempty(s)
1484
                                            @invokelatest(LineEdit.check_show_hint(s))
1485
                                        end
1486
                                        break
1487
                                    end
1488
                                end
1489
                                return :ok
1490
                            end
1491
                        )
1492
                    end
1493
                end
1494
                Base.errormonitor(t_replswitch)
×
1495
            else
1496
                edit_insert(s, ']')
3✔
1497
                LineEdit.check_show_hint(s)
3✔
1498
            end
1499
        end,
1500

1501
        # Bracketed Paste Mode
1502
        "\e[200~" => (s::MIState,o...)->begin
8✔
1503
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
8✔
1504
            sbuffer = LineEdit.buffer(s)
8✔
1505
            curspos = position(sbuffer)
8✔
1506
            seek(sbuffer, 0)
16✔
1507
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
8✔
1508
            seek(sbuffer, curspos)
16✔
1509
            if curspos == 0
8✔
1510
                # if pasting at the beginning, strip leading whitespace
1511
                input = lstrip(input)
7✔
1512
            end
1513
            if !shouldeval
8✔
1514
                # when pasting in the middle of input, just paste in place
1515
                # don't try to execute all the WIP, since that's rather confusing
1516
                # and is often ill-defined how it should behave
1517
                edit_insert(s, input)
×
1518
                return
×
1519
            end
1520
            LineEdit.push_undo(s)
8✔
1521
            edit_insert(sbuffer, input)
8✔
1522
            input = String(take!(sbuffer))
16✔
1523
            oldpos = firstindex(input)
8✔
1524
            firstline = true
8✔
1525
            isprompt_paste = false
8✔
1526
            curr_prompt_len = 0
8✔
1527
            pasting_help = false
8✔
1528

1529
            while oldpos <= lastindex(input) # loop until all lines have been executed
50✔
1530
                if JL_PROMPT_PASTE[]
24✔
1531
                    # Check if the next statement starts with a prompt i.e. "julia> ", in that case
1532
                    # skip it. But first skip whitespace unless pasting in a docstring which may have
1533
                    # indented prompt examples that we don't want to execute
1534
                    while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
86✔
1535
                        oldpos = nextind(input, oldpos)
48✔
1536
                        oldpos >= sizeof(input) && return
24✔
1537
                    end
24✔
1538
                    substr = SubString(input, oldpos)
48✔
1539
                    # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
1540
                    if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
24✔
1541
                        detected_jl_prompt = match(jl_prompt_regex, substr).match
11✔
1542
                        isprompt_paste = true
11✔
1543
                        curr_prompt_len = sizeof(detected_jl_prompt)
11✔
1544
                        oldpos += curr_prompt_len
11✔
1545
                        transition(s, julia_prompt)
11✔
1546
                        pasting_help = false
11✔
1547
                    # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode
1548
                    elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
13✔
1549
                        detected_pkg_prompt = match(pkg_prompt_regex, substr).match
×
1550
                        isprompt_paste = true
×
1551
                        curr_prompt_len = sizeof(detected_pkg_prompt)
×
1552
                        oldpos += curr_prompt_len
×
1553
                        Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...)
×
1554
                        pasting_help = false
×
1555
                    # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode
1556
                    elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
13✔
1557
                        isprompt_paste = true
2✔
1558
                        oldpos += shell_prompt_len
2✔
1559
                        curr_prompt_len = shell_prompt_len
2✔
1560
                        transition(s, shell_mode)
2✔
1561
                        pasting_help = false
2✔
1562
                    # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
1563
                    elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
11✔
1564
                        isprompt_paste = true
1✔
1565
                        oldpos += help_prompt_len
1✔
1566
                        curr_prompt_len = help_prompt_len
1✔
1567
                        transition(s, help_mode)
1✔
1568
                        pasting_help = true
1✔
1569
                    # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
1570
                    elseif isprompt_paste
10✔
1571
                        while input[oldpos] != '\n'
316✔
1572
                            oldpos = nextind(input, oldpos)
302✔
1573
                            oldpos >= sizeof(input) && return
151✔
1574
                        end
149✔
1575
                        continue
7✔
1576
                    end
1577
                end
1578
                dump_tail = false
15✔
1579
                nl_pos = findfirst('\n', input[oldpos:end])
30✔
1580
                if s.current_mode == julia_prompt
15✔
1581
                    ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
12✔
1582
                    if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
22✔
1583
                            (pos > ncodeunits(input) && !endswith(input, '\n'))
1584
                        # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1585
                        # Insert all the remaining text as one line (might be empty)
1586
                        dump_tail = true
5✔
1587
                    end
1588
                elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
6✔
1589
                    dump_tail = true
×
1590
                elseif s.current_mode == shell_mode # handle multiline shell commands
3✔
1591
                    lines = split(input[oldpos:end], '\n')
4✔
1592
                    pos = oldpos + sizeof(lines[1]) + 1
2✔
1593
                    if length(lines) > 1
2✔
1594
                        for line in lines[2:end]
2✔
1595
                            # to be recognized as a multiline shell command, the lines must be indented to the
1596
                            # same prompt position
1597
                            if !startswith(line, ' '^curr_prompt_len)
3✔
1598
                                break
2✔
1599
                            end
1600
                            pos += sizeof(line) + 1
1✔
1601
                        end
1✔
1602
                    end
1603
                else
1604
                    pos = oldpos + nl_pos
1✔
1605
                end
1606
                if dump_tail
15✔
1607
                    tail = input[oldpos:end]
10✔
1608
                    if !firstline
5✔
1609
                        # strip leading whitespace, but only if it was the result of executing something
1610
                        # (avoids modifying the user's current leading wip line)
1611
                        tail = lstrip(tail)
1✔
1612
                    end
1613
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
5✔
1614
                        tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
7✔
1615
                    end
1616
                    LineEdit.replace_line(s, tail, true)
5✔
1617
                    LineEdit.refresh_line(s)
5✔
1618
                    break
5✔
1619
                end
1620
                # get the line and strip leading and trailing whitespace
1621
                line = strip(input[oldpos:prevind(input, pos)])
20✔
1622
                if !isempty(line)
10✔
1623
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
10✔
1624
                        line = replace(line, r"^"m * ' '^curr_prompt_len => "")
10✔
1625
                    end
1626
                    # put the line on the screen and history
1627
                    LineEdit.replace_line(s, line)
20✔
1628
                    LineEdit.commit_line(s)
10✔
1629
                    # execute the statement
1630
                    terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
10✔
1631
                    raw!(terminal, false) && disable_bracketed_paste(terminal)
10✔
1632
                    @invokelatest LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
10✔
1633
                    raw!(terminal, true) && enable_bracketed_paste(terminal)
10✔
1634
                    LineEdit.push_undo(s) # when the last line is incomplete
10✔
1635
                end
1636
                oldpos = pos
10✔
1637
                firstline = false
10✔
1638
            end
17✔
1639
        end,
1640

1641
        # Open the editor at the location of a stackframe or method
1642
        # This is accessing a contextual variable that gets set in
1643
        # the show_backtrace and show_method_table functions.
1644
        "^Q" => (s::MIState, o...) -> begin
1645
            linfos = repl.last_shown_line_infos
1646
            str = String(take!(LineEdit.buffer(s)))
1647
            n = tryparse(Int, str)
1648
            n === nothing && @goto writeback
1649
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1650
                @goto writeback
1651
            end
1652
            try
1653
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1654
            catch ex
1655
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1656
                @info "edit failed" _exception=ex
1657
            end
1658
            LineEdit.refresh_line(s)
1659
            return
1660
            @label writeback
1661
            write(LineEdit.buffer(s), str)
1662
            return
1663
        end,
1664
    )
1665

1666
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
24✔
1667

1668
    a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
144✔
1669
    prepend!(a, extra_repl_keymap)
24✔
1670

1671
    julia_prompt.keymap_dict = LineEdit.keymap(a)
24✔
1672

1673
    mk = mode_keymap(julia_prompt)
24✔
1674

1675
    b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
144✔
1676
    prepend!(b, extra_repl_keymap)
24✔
1677

1678
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
24✔
1679

1680
    allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
24✔
1681
    return ModalInterface(allprompts)
24✔
1682
end
1683

1684
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
20✔
1685
    repl.frontend_task = current_task()
20✔
1686
    d = REPLDisplay(repl)
20✔
1687
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
34✔
1688
    dopushdisplay && pushdisplay(d)
20✔
1689
    if !isdefined(repl,:interface)
20✔
1690
        interface = repl.interface = setup_interface(repl)
24✔
1691
    else
1692
        interface = repl.interface
8✔
1693
    end
1694
    repl.backendref = backend
20✔
1695
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
20✔
1696
    run_interface(terminal(repl), interface, repl.mistate)
20✔
1697
    # Terminate Backend
1698
    put!(backend.repl_channel, (nothing, -1))
20✔
1699
    dopushdisplay && popdisplay(d)
20✔
1700
    nothing
20✔
1701
end
1702

1703
## StreamREPL ##
1704

1705
mutable struct StreamREPL <: AbstractREPL
1706
    stream::IO
1707
    prompt_color::String
1708
    input_color::String
1709
    answer_color::String
1710
    waserror::Bool
1711
    frontend_task::Task
1712
    StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
×
1713
end
1714
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
×
1715
run_repl(stream::IO) = run_repl(StreamREPL(stream))
×
1716

1717
outstream(s::StreamREPL) = s.stream
×
1718
hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
×
1719

1720
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
×
1721
answer_color(r::StreamREPL) = r.answer_color
×
1722
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
×
1723
input_color(r::StreamREPL) = r.input_color
×
1724

1725
# heuristic function to decide if the presence of a semicolon
1726
# at the end of the expression was intended for suppressing output
1727
function ends_with_semicolon(code)
129✔
1728
    semi = false
129✔
1729
    for tok in tokenize(code)
129✔
1730
        kind(tok) in KSet"Whitespace NewlineWs Comment EndMarker" && continue
2,599✔
1731
        semi = kind(tok) == K";"
1,023✔
1732
    end
1,397✔
1733
    return semi
129✔
1734
end
1735

1736
function banner(io::IO = stdout; short = false)
4✔
1737
    if Base.GIT_VERSION_INFO.tagged_commit
2✔
1738
        commit_string = Base.TAGGED_RELEASE_BANNER
×
1739
    elseif isempty(Base.GIT_VERSION_INFO.commit)
2✔
1740
        commit_string = ""
×
1741
    else
1742
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
2✔
1743
        days = max(0, days)
2✔
1744
        unit = days == 1 ? "day" : "days"
2✔
1745
        distance = Base.GIT_VERSION_INFO.fork_master_distance
2✔
1746
        commit = Base.GIT_VERSION_INFO.commit_short
2✔
1747

1748
        if distance == 0
2✔
1749
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
2✔
1750
        else
1751
            branch = Base.GIT_VERSION_INFO.branch
×
1752
            commit_string = "$(branch)/$(commit) (fork: $(distance) commits, $(days) $(unit))"
×
1753
        end
1754
    end
1755

1756
    commit_date = isempty(Base.GIT_VERSION_INFO.date_string) ? "" : " ($(split(Base.GIT_VERSION_INFO.date_string)[1]))"
2✔
1757

1758
    if get(io, :color, false)::Bool
2✔
1759
        c = Base.text_colors
×
1760
        tx = c[:normal] # text
×
1761
        jl = c[:normal] # julia
×
1762
        d1 = c[:bold] * c[:blue]    # first dot
×
1763
        d2 = c[:bold] * c[:red]     # second dot
×
1764
        d3 = c[:bold] * c[:green]   # third dot
×
1765
        d4 = c[:bold] * c[:magenta] # fourth dot
×
1766

1767
        if short
×
1768
            print(io,"""
×
1769
              $(d3)o$(tx)  | Version $(VERSION)$(commit_date)
1770
             $(d2)o$(tx) $(d4)o$(tx) | $(commit_string)
1771
            """)
1772
        else
1773
            print(io,"""               $(d3)_$(tx)
×
1774
               $(d1)_$(tx)       $(jl)_$(tx) $(d2)_$(d3)(_)$(d4)_$(tx)     |  Documentation: https://docs.julialang.org
1775
              $(d1)(_)$(jl)     | $(d2)(_)$(tx) $(d4)(_)$(tx)    |
1776
               $(jl)_ _   _| |_  __ _$(tx)   |  Type \"?\" for help, \"]?\" for Pkg help.
1777
              $(jl)| | | | | | |/ _` |$(tx)  |
1778
              $(jl)| | |_| | | | (_| |$(tx)  |  Version $(VERSION)$(commit_date)
1779
             $(jl)_/ |\\__'_|_|_|\\__'_|$(tx)  |  $(commit_string)
1780
            $(jl)|__/$(tx)                   |
1781

1782
            """)
1783
        end
1784
    else
1785
        if short
2✔
1786
            print(io,"""
1✔
1787
              o  |  Version $(VERSION)$(commit_date)
1788
             o o |  $(commit_string)
1789
            """)
1790
        else
1791
            print(io,"""
1✔
1792
                           _
1793
               _       _ _(_)_     |  Documentation: https://docs.julialang.org
1794
              (_)     | (_) (_)    |
1795
               _ _   _| |_  __ _   |  Type \"?\" for help, \"]?\" for Pkg help.
1796
              | | | | | | |/ _` |  |
1797
              | | |_| | | | (_| |  |  Version $(VERSION)$(commit_date)
1798
             _/ |\\__'_|_|_|\\__'_|  |  $(commit_string)
1799
            |__/                   |
1800

1801
            """)
1802
        end
1803
    end
1804
end
1805

1806
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
×
1807
    repl.frontend_task = current_task()
×
1808
    have_color = hascolor(repl)
×
1809
    banner(repl.stream)
×
1810
    d = REPLDisplay(repl)
×
1811
    dopushdisplay = !in(d,Base.Multimedia.displays)
×
1812
    dopushdisplay && pushdisplay(d)
×
1813
    while !eof(repl.stream)::Bool
×
1814
        if have_color
×
1815
            print(repl.stream,repl.prompt_color)
×
1816
        end
1817
        print(repl.stream, JULIA_PROMPT)
×
1818
        if have_color
×
1819
            print(repl.stream, input_color(repl))
×
1820
        end
1821
        line = readline(repl.stream, keep=true)
×
1822
        if !isempty(line)
×
1823
            ast = Base.parse_input_line(line)
×
1824
            if have_color
×
1825
                print(repl.stream, Base.color_normal)
×
1826
            end
1827
            response = eval_on_backend(ast, backend)
×
1828
            print_response(repl, response, !ends_with_semicolon(line), have_color)
×
1829
        end
1830
    end
×
1831
    # Terminate Backend
1832
    put!(backend.repl_channel, (nothing, -1))
×
1833
    dopushdisplay && popdisplay(d)
×
1834
    nothing
×
1835
end
1836

1837
module Numbered
1838

1839
using ..REPL
1840

1841
__current_ast_transforms() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1842

1843
function repl_eval_counter(hp)
567✔
1844
    return length(hp.history) - hp.start_idx
567✔
1845
end
1846

1847
function out_transform(@nospecialize(x), n::Ref{Int})
16✔
1848
    return Expr(:block, # avoid line numbers or scope that would leak into the output and change the meaning of x
16✔
1849
        :(local __temp_val_a72df459 = $x),
1850
        Expr(:call, capture_result, n, :__temp_val_a72df459),
1851
        :__temp_val_a72df459)
1852
end
1853

1854
function create_global_out!(mod)
1✔
1855
    if !isdefinedglobal(mod, :Out)
1✔
1856
        out = Dict{Int, Any}()
1✔
1857
        @eval mod begin
1✔
1858
            const Out = $(out)
1859
            export Out
1860
        end
1861
        return out
1✔
1862
    end
1863
    return getglobal(mod, :Out)
×
1864
end
1865

1866
function capture_result(n::Ref{Int}, @nospecialize(x))
16✔
1867
    n = n[]
16✔
1868
    mod = Base.MainInclude
16✔
1869
    # TODO: This invokelatest is only required due to backdated constants
1870
    # and should be removed after
1871
    out = isdefinedglobal(mod, :Out) ? invokelatest(getglobal, mod, :Out) : invokelatest(create_global_out!, mod)
17✔
1872
    if x !== out && x !== nothing # remove this?
16✔
1873
        out[n] = x
14✔
1874
    end
1875
    nothing
16✔
1876
end
1877

1878
function set_prompt(repl::LineEditREPL, n::Ref{Int})
1✔
1879
    julia_prompt = repl.interface.modes[1]
1✔
1880
    julia_prompt.prompt = REPL.contextual_prompt(repl, function()
568✔
1881
        n[] = repl_eval_counter(julia_prompt.hist)+1
567✔
1882
        string("In [", n[], "]: ")
567✔
1883
    end)
1884
    nothing
1✔
1885
end
1886

1887
function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1✔
1888
    julia_prompt = repl.interface.modes[1]
1✔
1889
    if REPL.hascolor(repl)
1✔
1890
        julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1✔
1891
    end
1892
    julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
15✔
1893
    nothing
1✔
1894
end
1895

1896
function __current_ast_transforms(backend)
1897
    if backend === nothing
1✔
1898
        Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1899
    else
1900
        backend.ast_transforms
1✔
1901
    end
1902
end
1903

1904
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
1905
    n = Ref{Int}(0)
1✔
1906
    set_prompt(repl, n)
1✔
1907
    set_output_prefix(repl, n)
1✔
1908
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
17✔
1909
    return
1✔
1910
end
1911

1912
"""
1913
    Out[n]
1914

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

1918
See also [`ans`](@ref).
1919
"""
1920
Base.MainInclude.Out
1921

1922
end
1923

1924
import .Numbered.numbered_prompt!
1925

1926
# this assignment won't survive precompilation,
1927
# but will stick if REPL is baked into a sysimg.
1928
# Needs to occur after this module is finished.
1929
Base.REPL_MODULE_REF[] = REPL
1930

1931
if Base.generating_output()
1932
    include("precompile.jl")
1933
end
1934

1935
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

© 2026 Coveralls, Inc