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

JuliaLang / julia / #37922

03 Oct 2024 12:46AM UTC coverage: 87.736% (+1.0%) from 86.72%
#37922

push

local

web-flow
[build] avoid libedit linkage and align libccalllazy* SONAMEs (#55968)

While building the 1.11.0-rc4 in Homebrew[^1] in preparation for 1.11.0
release (and to confirm Sequoia successfully builds) I noticed some odd
linkage for our Linux builds, which included of:

1. LLVM libraries were linking to `libedit.so`, e.g.
    ```
    Dynamic Section:
      NEEDED       libedit.so.0
      NEEDED       libz.so.1
      NEEDED       libzstd.so.1
      NEEDED       libstdc++.so.6
      NEEDED       libm.so.6
      NEEDED       libgcc_s.so.1
      NEEDED       libc.so.6
      NEEDED       ld-linux-x86-64.so.2
      SONAME       libLLVM-16jl.so
    ```
    CMakeCache.txt showed
    ```
    //Use libedit if available.
    LLVM_ENABLE_LIBEDIT:BOOL=ON
    ```
Which might be overriding `HAVE_LIBEDIT` at
https://github.com/JuliaLang/llvm-project/blob/julia-release/16.x/llvm/cmake/config-ix.cmake#L222-L225.
So just added `LLVM_ENABLE_LIBEDIT`

2. Wasn't sure if there was a reason for this but `libccalllazy*` had
mismatched SONAME:
    ```console
    ❯ objdump -p lib/julia/libccalllazy* | rg '\.so'
    lib/julia/libccalllazybar.so:	file format elf64-x86-64
      NEEDED       ccalllazyfoo.so
      SONAME       ccalllazybar.so
    lib/julia/libccalllazyfoo.so:	file format elf64-x86-64
      SONAME       ccalllazyfoo.so
    ```
    Modifying this, but can drop if intentional.

---

[^1]: https://github.com/Homebrew/homebrew-core/pull/192116

78782 of 89794 relevant lines covered (87.74%)

16687498.83 hits per line

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

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

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

6
Example minimal code
7

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

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

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

74
function _UndefVarError_warnfor(io::IO, m::Module, var::Symbol)
24✔
75
    Base.isbindingresolved(m, var) || return false
46✔
76
    (Base.isexported(m, var) || Base.ispublic(m, var)) || return false
4✔
77
    active_mod = Base.active_module()
1✔
78
    print(io, "\nHint: ")
1✔
79
    if isdefined(active_mod, Symbol(m))
1✔
80
        print(io, "a global variable of this name also exists in $m.")
1✔
81
    else
82
        if Symbol(m) == var
×
83
            print(io, "$m is loaded but not imported in the active module $active_mod.")
×
84
        else
85
            print(io, "a global variable of this name may be made accessible by importing $m in the current active module $active_mod")
×
86
        end
87
    end
88
    return true
1✔
89
end
90

91
function __init__()
5✔
92
    Base.REPL_MODULE_REF[] = REPL
5✔
93
    Base.Experimental.register_error_hint(UndefVarError_hint, UndefVarError)
5✔
94
    return nothing
5✔
95
end
96

97
using Base.Meta, Sockets, StyledStrings
98
using JuliaSyntaxHighlighting
99
import InteractiveUtils
100

101
export
102
    AbstractREPL,
103
    BasicREPL,
104
    LineEditREPL,
105
    StreamREPL
106

107
public TerminalMenus
108

109
import Base:
110
    AbstractDisplay,
111
    display,
112
    show,
113
    AnyDict,
114
    ==
115

116
_displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
32✔
117

118
include("Terminals.jl")
119
using .Terminals
120

121
abstract type AbstractREPL end
122

123
include("options.jl")
124

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

145
include("REPLCompletions.jl")
146
using .REPLCompletions
147

148
include("TerminalMenus/TerminalMenus.jl")
149
include("docview.jl")
150

151
include("Pkg_beforeload.jl")
152

153
@nospecialize # use only declared type signatures
154

155
answer_color(::AbstractREPL) = ""
×
156

157
const JULIA_PROMPT = "julia> "
158
const PKG_PROMPT = "pkg> "
159
const SHELL_PROMPT = "shell> "
160
const HELP_PROMPT = "help?> "
161

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

174
    REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
52✔
175
        new(repl_channel, response_channel, in_eval, ast_transforms)
176
end
177
REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
26✔
178

179
"""
180
    softscope(ex)
181

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

203
# Temporary alias until Documenter updates
204
const softscope! = softscope
205

206
function print_qualified_access_warning(mod::Module, owner::Module, name::Symbol)
2✔
207
    @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✔
208
end
209

210
function has_ancestor(query::Module, target::Module)
2✔
211
    query == target && return true
38✔
212
    while true
30✔
213
        next = parentmodule(query)
30✔
214
        next == target && return true
30✔
215
        next == query && return false
28✔
216
        query = next
×
217
    end
15✔
218
end
219

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

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

252
function collect_names_to_warn!(warnings, locals, current_module::Module, ast)
1,173✔
253
    ast isa Expr || return
1,697✔
254

255
    # don't recurse through module definitions
256
    ast.head === :module && return
649✔
257

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

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

302
    for arg in ast.args
541✔
303
        collect_names_to_warn!(warnings, locals, current_module, arg)
1,004✔
304
    end
1,004✔
305

306
    return nothing
541✔
307
end
308

309
function collect_qualified_access_warnings(current_mod, ast)
12✔
310
    warnings = Set()
122✔
311
    locals = Set{Symbol}()
122✔
312
    collect_names_to_warn!(warnings, locals, current_mod, ast)
122✔
313
    filter!(warnings) do (; outer_mod)
122✔
314
        nameof(outer_mod) ∉ locals
11✔
315
    end
316
    return warnings
122✔
317
end
318

319
function warn_on_non_owning_accesses(current_mod, ast)
110✔
320
    warnings = collect_qualified_access_warnings(current_mod, ast)
110✔
321
    for (; outer_mod, mod, owner, name_being_accessed) in warnings
220✔
322
        print_qualified_access_warning(mod, owner, name_being_accessed)
2✔
323
    end
4✔
324
    return ast
110✔
325
end
326
warn_on_non_owning_accesses(ast) = warn_on_non_owning_accesses(Base.active_module(), ast)
108✔
327

328
const repl_ast_transforms = Any[softscope, warn_on_non_owning_accesses] # defaults for new REPL backends
329

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

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

344
function toplevel_eval_with_hooks(mod::Module, @nospecialize(ast), toplevel_file=Ref{Ptr{UInt8}}(Base.unsafe_convert(Ptr{UInt8}, :REPL)), toplevel_line=Ref{Cint}(1))
345✔
345
    if !isexpr(ast, :toplevel)
453✔
346
        ast = __repl_entry_lower_with_loc(mod, ast, toplevel_file, toplevel_line)
217✔
347
        check_for_missing_packages_and_run_hooks(ast)
217✔
348
        return __repl_entry_eval_expanded_with_loc(mod, ast, toplevel_file, toplevel_line)
216✔
349
    end
350
    local value=nothing
128✔
351
    for i = 1:length(ast.args)
128✔
352
        value = toplevel_eval_with_hooks(mod, ast.args[i], toplevel_file, toplevel_line)
237✔
353
    end
339✔
354
    return value
121✔
355
end
356

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

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

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

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

439
"""
440
    start_repl_backend(repl_channel::Channel, response_channel::Channel)
441

442
    Starts loop for REPL backend
443
    Returns a REPLBackend with backend_task assigned
444

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

456
"""
457
    start_repl_backend(backend::REPLBackend)
458

459
    Call directly to run backend loop on current Task.
460
    Use @async for run backend on new Task.
461

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

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

487
struct REPLDisplay{Repl<:AbstractREPL} <: AbstractDisplay
488
    repl::Repl
35✔
489
end
490

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

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

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

517
show_repl(io::IO, ::MIME"text/plain", ex::Expr) =
2✔
518
    print(io, JuliaSyntaxHighlighting.highlight(
519
        sprint(show, ex, context=IOContext(io, :color => false))))
520

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

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

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

592
# A reference to a backend that is not mutable
593
struct REPLBackendRef
594
    repl_channel::Channel{Any}
24✔
595
    response_channel::Channel{Any}
596
end
597
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
24✔
598

599
function destroy(ref::REPLBackendRef, state::Task)
600
    if istaskfailed(state)
23✔
601
        close(ref.repl_channel, TaskFailedException(state))
×
602
        close(ref.response_channel, TaskFailedException(state))
×
603
    end
604
    close(ref.repl_channel)
23✔
605
    close(ref.response_channel)
23✔
606
end
607

608
"""
609
    run_repl(repl::AbstractREPL)
610
    run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
611

612
    Main function to start the REPL
613

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

640
## BasicREPL ##
641

642
mutable struct BasicREPL <: AbstractREPL
643
    terminal::TextTerminal
644
    waserror::Bool
645
    frontend_task::Task
646
    BasicREPL(t) = new(t, false)
3✔
647
end
648

649
outstream(r::BasicREPL) = r.terminal
6✔
650
hascolor(r::BasicREPL) = hascolor(r.terminal)
×
651

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

699
## LineEditREPL ##
700

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

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

749
mutable struct REPLCompletionProvider <: CompletionProvider
750
    modifiers::LineEdit.Modifiers
25✔
751
end
752
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
25✔
753

754
mutable struct ShellCompletionProvider <: CompletionProvider end
25✔
755
struct LatexCompletions <: CompletionProvider end
756

757
Base.active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
4,747✔
758
Base.active_module(::AbstractREPL) = Main
2✔
759
Base.active_module(d::REPLDisplay) = Base.active_module(d.repl)
123✔
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; interactive_utils::Bool=true)
×
772
    mistate = (Base.active_repl::LineEditREPL).mistate
×
773
    mistate === nothing && return nothing
×
774
    mistate.active_module = mod
×
775
    interactive_utils && Base.load_InteractiveUtils(mod)
×
776
    return nothing
×
777
end
778

779
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
4,394✔
780

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

789
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
844✔
790
    # First parse everything up to the current position
791
    partial = beforecursor(s.input_buffer)
844✔
792
    full = LineEdit.input_string(s)
422✔
793
    ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
844✔
794
    return unique!(String[completion_text(x) for x in ret]), partial[range], should_complete
422✔
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

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

815
mutable struct REPLHistoryProvider <: HistoryProvider
816
    history::Vector{String}
30✔
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
827
REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
30✔
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

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

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

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

892
function add_history(hist::REPLHistoryProvider, s::PromptState)
119✔
893
    str = rstrip(String(take!(copy(s.input_buffer))))
223✔
894
    isempty(strip(str)) && return
119✔
895
    mode = mode_idx(hist, LineEdit.mode(s))
101✔
896
    !isempty(hist.history) &&
101✔
897
        isequal(mode, hist.modes[end]) && str == hist.history[end] && return
898
    push!(hist.modes, mode)
95✔
899
    push!(hist.history, str)
95✔
900
    hist.history_file === nothing && return
95✔
901
    entry = """
16✔
902
    # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
903
    # mode: $mode
904
    $(replace(str, r"^"ms => "\t"))
905
    """
906
    # TODO: write-lock history file
907
    try
16✔
908
        seekend(hist.history_file)
16✔
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
915
    print(hist.history_file, entry)
32✔
916
    flush(hist.history_file)
16✔
917
    nothing
918
end
919

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

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

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

954
    return :ok
78✔
955
end
956

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

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

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

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

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

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

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

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

1064
    # Alright, first try to see if the current match still works
1065
    a = position(response_buffer) + 1 # position is zero-indexed
28✔
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
1068
    b = a + sizeof(searchdata)
28✔
1069
    b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
48✔
1070
    b = min(lastindex(response_str), b) # ensure that b is valid
50✔
1071

1072
    searchstart = backwards ? b : a
28✔
1073
    if searchdata == response_str[a:b]
44✔
1074
        if skip_current
10✔
1075
            searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
4✔
1076
        else
1077
            return true
6✔
1078
        end
1079
    end
1080

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

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

1106
    return false
4✔
1107
end
1108

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

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

1123
find_hist_file() = get(ENV, "JULIA_HISTORY",
4✔
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

1127
backend(r::AbstractREPL) = r.backendref
100✔
1128

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

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

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

1164
function prepare_next(repl::LineEditREPL)
116✔
1165
    println(terminal(repl))
116✔
1166
end
1167

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

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

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

1196
function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
1197
    function ()
2,166✔
1198
        mod = Base.active_module(repl)
4,225✔
1199
        prefix = mod == Main ? "" : string('(', mod, ") ")
2,148✔
1200
        pr = prompt isa String ? prompt : prompt()
2,116✔
1201
        prefix * pr
2,116✔
1202
    end
1203
end
1204

1205
setup_interface(
71✔
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
1214
function setup_interface(
25✔
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.
1223
    @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
25✔
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
1247
    replc = REPLCompletionProvider()
25✔
1248

1249
    # Set up the main Julia prompt
1250
    julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
50✔
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
1260
    help_mode = Prompt(contextual_prompt(repl, HELP_PROMPT),
50✔
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
1267
        on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
2✔
1268
                          repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1269

1270

1271
    # Set up shell mode
1272
    shell_mode = Prompt(SHELL_PROMPT;
50✔
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
1282
            Expr(:call, :(Base.repl_cmd),
9✔
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
1290
    dummy_pkg_mode = Prompt(Pkg_promptf,
50✔
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(),
1296
        on_done = respond(line->nothing, repl, julia_prompt),
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.
1300
                REPLExt = load_pkg()
1301
                if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1302
                    for mode in repl.interface.modes
1303
                        if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1304
                            # pkg mode
1305
                            buf = copy(LineEdit.buffer(s))
1306
                            transition(s, mode) do
1307
                                LineEdit.state(s, mode).input_buffer = buf
1308
                            end
1309
                        end
1310
                    end
1311
                end
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
1321
    hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
25✔
1322
                                                 :shell => shell_mode,
1323
                                                 :help  => help_mode,
1324
                                                 :pkg  => dummy_pkg_mode))
1325
    if repl.history_file
25✔
1326
        try
4✔
1327
            hist_path = find_hist_file()
4✔
1328
            mkpath(dirname(hist_path))
4✔
1329
            hp.file_path = hist_path
4✔
1330
            hist_open_file(hp)
4✔
1331
            finalizer(replc) do replc
4✔
1332
                close(hp.history_file)
4✔
1333
            end
1334
            hist_from_file(hp, hist_path)
4✔
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
1343
    history_reset_state(hp)
25✔
1344
    julia_prompt.hist = hp
25✔
1345
    shell_mode.hist = hp
25✔
1346
    help_mode.hist = hp
25✔
1347
    dummy_pkg_mode.hist = hp
25✔
1348

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

1351

1352
    search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
25✔
1353
    search_prompt.complete = LatexCompletions()
25✔
1354

1355
    shell_prompt_len = length(SHELL_PROMPT)
×
1356
    help_prompt_len = length(HELP_PROMPT)
×
1357
    jl_prompt_regex = Regex("^In \\[[0-9]+\\]: |^(?:\\(.+\\) )?$JULIA_PROMPT")
25✔
1358
    pkg_prompt_regex = Regex("^(?:\\(.+\\) )?$PKG_PROMPT")
25✔
1359

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

1365
    repl_keymap = AnyDict(
25✔
1366
        ';' => function (s::MIState,o...)
55✔
1367
            if isempty(s) || position(LineEdit.buffer(s)) == 0
103✔
1368
                buf = copy(LineEdit.buffer(s))
7✔
1369
                transition(s, shell_mode) do
7✔
1370
                    LineEdit.state(s, shell_mode).input_buffer = buf
7✔
1371
                end
1372
            else
1373
                edit_insert(s, ';')
48✔
1374
            end
1375
        end,
1376
        '?' => function (s::MIState,o...)
1✔
1377
            if isempty(s) || position(LineEdit.buffer(s)) == 0
1✔
1378
                buf = copy(LineEdit.buffer(s))
1✔
1379
                transition(s, help_mode) do
1✔
1380
                    LineEdit.state(s, help_mode).input_buffer = buf
1✔
1381
                end
1382
            else
1383
                edit_insert(s, '?')
×
1384
            end
1385
        end,
1386
        ']' => function (s::MIState,o...)
3✔
1387
            if isempty(s) || position(LineEdit.buffer(s)) == 0
6✔
1388
                buf = copy(LineEdit.buffer(s))
×
1389
                transition(s, dummy_pkg_mode) do
×
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
×
1395
                    REPLExt = load_pkg()
1396
                    if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
1397
                        put!(s.async_channel,
1398
                            function (s::MIState)
1399
                                LineEdit.mode(s) === dummy_pkg_mode || return :ok
1400
                                for mode in repl.interface.modes
1401
                                    if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
1402
                                        buf = copy(LineEdit.buffer(s))
1403
                                        transition(s, mode) do
1404
                                            LineEdit.state(s, mode).input_buffer = buf
1405
                                        end
1406
                                        if !isempty(s) && @invokelatest(LineEdit.check_for_hint(s))
1407
                                            @invokelatest(LineEdit.refresh_line(s))
1408
                                        end
1409
                                        break
1410
                                    end
1411
                                end
1412
                                return :ok
1413
                            end
1414
                        )
1415
                    end
1416
                end
1417
                Base.errormonitor(t_replswitch)
×
1418
            else
1419
                edit_insert(s, ']')
3✔
1420
            end
1421
        end,
1422

1423
        # Bracketed Paste Mode
1424
        "\e[200~" => (s::MIState,o...)->begin
8✔
1425
            input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
8✔
1426
            sbuffer = LineEdit.buffer(s)
8✔
1427
            curspos = position(sbuffer)
8✔
1428
            seek(sbuffer, 0)
16✔
1429
            shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
8✔
1430
            seek(sbuffer, curspos)
16✔
1431
            if curspos == 0
8✔
1432
                # if pasting at the beginning, strip leading whitespace
1433
                input = lstrip(input)
7✔
1434
            end
1435
            if !shouldeval
8✔
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
1442
            LineEdit.push_undo(s)
8✔
1443
            edit_insert(sbuffer, input)
8✔
1444
            input = String(take!(sbuffer))
16✔
1445
            oldpos = firstindex(input)
×
1446
            firstline = true
×
1447
            isprompt_paste = false
×
1448
            curr_prompt_len = 0
×
1449
            pasting_help = false
×
1450

1451
            while oldpos <= lastindex(input) # loop until all lines have been executed
50✔
1452
                if JL_PROMPT_PASTE[]
24✔
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
1456
                    while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
66✔
1457
                        oldpos = nextind(input, oldpos)
28✔
1458
                        oldpos >= sizeof(input) && return
14✔
1459
                    end
14✔
1460
                    substr = SubString(input, oldpos)
48✔
1461
                    # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
1462
                    if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
24✔
1463
                        detected_jl_prompt = match(jl_prompt_regex, substr).match
11✔
1464
                        isprompt_paste = true
×
1465
                        curr_prompt_len = sizeof(detected_jl_prompt)
11✔
1466
                        oldpos += curr_prompt_len
11✔
1467
                        transition(s, julia_prompt)
11✔
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
1470
                    elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
13✔
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
1478
                    elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
13✔
1479
                        isprompt_paste = true
×
1480
                        oldpos += shell_prompt_len
2✔
1481
                        curr_prompt_len = shell_prompt_len
2✔
1482
                        transition(s, shell_mode)
2✔
1483
                        pasting_help = false
×
1484
                    # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
1485
                    elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
11✔
1486
                        isprompt_paste = true
×
1487
                        oldpos += help_prompt_len
1✔
1488
                        curr_prompt_len = help_prompt_len
1✔
1489
                        transition(s, help_mode)
1✔
1490
                        pasting_help = true
×
1491
                    # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
1492
                    elseif isprompt_paste
10✔
1493
                        while input[oldpos] != '\n'
316✔
1494
                            oldpos = nextind(input, oldpos)
302✔
1495
                            oldpos >= sizeof(input) && return
151✔
1496
                        end
149✔
1497
                        continue
7✔
1498
                    end
1499
                end
1500
                dump_tail = false
15✔
1501
                nl_pos = findfirst('\n', input[oldpos:end])
30✔
1502
                if s.current_mode == julia_prompt
15✔
1503
                    ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
12✔
1504
                    if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
22✔
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
1510
                elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
6✔
1511
                    dump_tail = true
×
1512
                elseif s.current_mode == shell_mode # handle multiline shell commands
3✔
1513
                    lines = split(input[oldpos:end], '\n')
4✔
1514
                    pos = oldpos + sizeof(lines[1]) + 1
2✔
1515
                    if length(lines) > 1
2✔
1516
                        for line in lines[2:end]
2✔
1517
                            # to be recognized as a multiline shell command, the lines must be indented to the
1518
                            # same prompt position
1519
                            if !startswith(line, ' '^curr_prompt_len)
3✔
1520
                                break
2✔
1521
                            end
1522
                            pos += sizeof(line) + 1
1✔
1523
                        end
1✔
1524
                    end
1525
                else
1526
                    pos = oldpos + nl_pos
1✔
1527
                end
1528
                if dump_tail
15✔
1529
                    tail = input[oldpos:end]
10✔
1530
                    if !firstline
5✔
1531
                        # strip leading whitespace, but only if it was the result of executing something
1532
                        # (avoids modifying the user's current leading wip line)
1533
                        tail = lstrip(tail)
1✔
1534
                    end
1535
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
5✔
1536
                        tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
7✔
1537
                    end
1538
                    LineEdit.replace_line(s, tail, true)
5✔
1539
                    LineEdit.refresh_line(s)
5✔
1540
                    break
5✔
1541
                end
1542
                # get the line and strip leading and trailing whitespace
1543
                line = strip(input[oldpos:prevind(input, pos)])
20✔
1544
                if !isempty(line)
10✔
1545
                    if isprompt_paste # remove indentation spaces corresponding to the prompt
10✔
1546
                        line = replace(line, r"^"m * ' '^curr_prompt_len => "")
10✔
1547
                    end
1548
                    # put the line on the screen and history
1549
                    LineEdit.replace_line(s, line)
20✔
1550
                    LineEdit.commit_line(s)
10✔
1551
                    # execute the statement
1552
                    terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
10✔
1553
                    raw!(terminal, false) && disable_bracketed_paste(terminal)
10✔
1554
                    @invokelatest LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
10✔
1555
                    raw!(terminal, true) && enable_bracketed_paste(terminal)
10✔
1556
                    LineEdit.push_undo(s) # when the last line is incomplete
10✔
1557
                end
1558
                oldpos = pos
10✔
1559
                firstline = false
×
1560
            end
17✔
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.
1566
        "^Q" => (s::MIState, o...) -> begin
1567
            linfos = repl.last_shown_line_infos
1568
            str = String(take!(LineEdit.buffer(s)))
1569
            n = tryparse(Int, str)
1570
            n === nothing && @goto writeback
1571
            if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1572
                @goto writeback
1573
            end
1574
            try
1575
                InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1576
            catch ex
1577
                ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1578
                @info "edit failed" _exception=ex
1579
            end
1580
            LineEdit.refresh_line(s)
1581
            return
1582
            @label writeback
1583
            write(LineEdit.buffer(s), str)
1584
            return
1585
        end,
1586
    )
1587

1588
    prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
25✔
1589

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

1593
    julia_prompt.keymap_dict = LineEdit.keymap(a)
25✔
1594

1595
    mk = mode_keymap(julia_prompt)
25✔
1596

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

1600
    shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)
25✔
1601

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

1606
function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
21✔
1607
    repl.frontend_task = current_task()
21✔
1608
    d = REPLDisplay(repl)
21✔
1609
    dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
36✔
1610
    dopushdisplay && pushdisplay(d)
21✔
1611
    if !isdefined(repl,:interface)
21✔
1612
        interface = repl.interface = setup_interface(repl)
26✔
1613
    else
1614
        interface = repl.interface
8✔
1615
    end
1616
    repl.backendref = backend
21✔
1617
    repl.mistate = LineEdit.init_state(terminal(repl), interface)
21✔
1618
    run_interface(terminal(repl), interface, repl.mistate)
21✔
1619
    # Terminate Backend
1620
    put!(backend.repl_channel, (nothing, -1))
21✔
1621
    dopushdisplay && popdisplay(d)
21✔
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
1650
    function _rm_strings_and_comments(code::Union{String,SubString{String}})
126✔
1651
        buf = IOBuffer(sizehint = sizeof(code))
252✔
1652
        pos = 1
×
1653
        while true
166✔
1654
            i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
332✔
1655
            isnothing(i) && break
212✔
1656
            match = SubString(code, i)
46✔
1657
            j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
92✔
1658
            if match == "#=" # possibly nested
46✔
1659
                nested = 1
×
1660
                while j !== nothing
11✔
1661
                    nested += SubString(code, j) == "#=" ? +1 : -1
10✔
1662
                    iszero(nested) && break
10✔
1663
                    j = findnext(r"=#|#=", code, nextind(code, last(j)))
12✔
1664
                end
6✔
1665
            elseif match[1] != '#' # quote match: check non-escaped
82✔
1666
                while j !== nothing
38✔
1667
                    notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
66✔
1668
                    isodd(first(j) - notbackslash) && break # not escaped
33✔
1669
                    j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
14✔
1670
                end
7✔
1671
            end
1672
            isnothing(j) && break
86✔
1673
            if match[1] == '#'
80✔
1674
                print(buf, SubString(code, pos, prevind(code, first(i))))
14✔
1675
            else
1676
                print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
26✔
1677
            end
1678
            pos = nextind(code, last(j))
40✔
1679
        end
40✔
1680
        print(buf, SubString(code, pos, lastindex(code)))
126✔
1681
        return String(take!(buf))
126✔
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))
×
1688
ends_with_semicolon(code::Union{String,SubString{String}}) =
126✔
1689
    contains(_rm_strings_and_comments(code), r";\s*$")
1690

1691
function banner(io::IO = stdout; short = false)
4✔
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
1697
        days = Int(floor((ccall(:jl_clock_now, Float64, ()) - Base.GIT_VERSION_INFO.fork_master_timestamp) / (60 * 60 * 24)))
2✔
1698
        days = max(0, days)
2✔
1699
        unit = days == 1 ? "day" : "days"
2✔
1700
        distance = Base.GIT_VERSION_INFO.fork_master_distance
×
1701
        commit = Base.GIT_VERSION_INFO.commit_short
2✔
1702

1703
        if distance == 0
×
1704
            commit_string = "Commit $(commit) ($(days) $(unit) old master)"
2✔
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

1713
    if get(io, :color, false)::Bool
2✔
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
1740
        if short
2✔
1741
            print(io,"""
1✔
1742
              o  |  Version $(VERSION)$(commit_date)
1743
             o o |  $(commit_string)
1744
            """)
1745
        else
1746
            print(io,"""
1✔
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_PROMPT)
×
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() = Base.active_repl_backend !== nothing ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
×
1797

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

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

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

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

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

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

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

1864
function numbered_prompt!(repl::LineEditREPL=Base.active_repl::LineEditREPL, backend=nothing)
1865
    n = Ref{Int}(0)
1✔
1866
    set_prompt(repl, n)
1✔
1867
    set_output_prefix(repl, n)
1✔
1868
    push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
17✔
1869
    return
1✔
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