StatProfilerHTML.jl report
Generated on Mon, 01 Apr 2024 21:01:18
File source code
Line Exclusive Inclusive Code
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 using Base.Meta, Sockets
21 import InteractiveUtils
22
23 export
24 AbstractREPL,
25 BasicREPL,
26 LineEditREPL,
27 StreamREPL
28
29 import Base:
30 AbstractDisplay,
31 display,
32 show,
33 AnyDict,
34 ==
35
36 _displaysize(io::IO) = displaysize(io)::Tuple{Int,Int}
37
38 include("Terminals.jl")
39 using .Terminals
40
41 abstract type AbstractREPL end
42
43 include("options.jl")
44
45 include("LineEdit.jl")
46 using .LineEdit
47 import ..LineEdit:
48 CompletionProvider,
49 HistoryProvider,
50 add_history,
51 complete_line,
52 history_next,
53 history_next_prefix,
54 history_prev,
55 history_prev_prefix,
56 history_first,
57 history_last,
58 history_search,
59 accept_result,
60 setmodifiers!,
61 terminal,
62 MIState,
63 PromptState,
64 TextInterface,
65 mode_idx
66
67 include("REPLCompletions.jl")
68 using .REPLCompletions
69
70 include("TerminalMenus/TerminalMenus.jl")
71 include("docview.jl")
72
73 @nospecialize # use only declared type signatures
74
75 answer_color(::AbstractREPL) = ""
76
77 const JULIA_PROMPT = "julia> "
78 const PKG_PROMPT = "pkg> "
79 const SHELL_PROMPT = "shell> "
80 const HELP_PROMPT = "help?> "
81
82 mutable struct REPLBackend
83 "channel for AST"
84 repl_channel::Channel{Any}
85 "channel for results: (value, iserror)"
86 response_channel::Channel{Any}
87 "flag indicating the state of this backend"
88 in_eval::Bool
89 "transformation functions to apply before evaluating expressions"
90 ast_transforms::Vector{Any}
91 "current backend task"
92 backend_task::Task
93
94 REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
95 new(repl_channel, response_channel, in_eval, ast_transforms)
96 end
97 REPLBackend() = REPLBackend(Channel(1), Channel(1), false)
98
99 """
100 softscope(ex)
101
102 Return a modified version of the parsed expression `ex` that uses
103 the REPL's "soft" scoping rules for global syntax blocks.
104 """
105 function softscope(@nospecialize ex)
106 if ex isa Expr
107 h = ex.head
108 if h === :toplevel
109 ex′ = Expr(h)
110 map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
111 return ex′
112 elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
113 return ex
114 elseif h === :global && all(x->isa(x, Symbol), ex.args)
115 return ex
116 else
117 return Expr(:block, Expr(:softscope, true), ex)
118 end
119 end
120 return ex
121 end
122
123 # Temporary alias until Documenter updates
124 const softscope! = softscope
125
126 const repl_ast_transforms = Any[softscope] # defaults for new REPL backends
127
128 # Allows an external package to add hooks into the code loading.
129 # The hook should take a Vector{Symbol} of package names and
130 # return true if all packages could be installed, false if not
131 # to e.g. install packages on demand
132 const install_packages_hooks = Any[]
133
134
58 (100 %) samples spent in eval_user_input
58 (100 %) (incl.) when called from repl_backend_loop line 246
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
135 lasterr = nothing
136 Base.sigatomic_begin()
137 while true
138 try
139 Base.sigatomic_end()
140 if lasterr !== nothing
141 put!(backend.response_channel, Pair{Any, Bool}(lasterr, true))
142 else
143 backend.in_eval = true
144 if !isempty(install_packages_hooks)
145 check_for_missing_packages_and_run_hooks(ast)
146 end
147 for xf in backend.ast_transforms
148 ast = Base.invokelatest(xf, ast)
149 end
150 58 (100 %)
58 (100 %) samples spent calling eval
value = Core.eval(mod, ast)
151 backend.in_eval = false
152 setglobal!(Base.MainInclude, :ans, value)
153 put!(backend.response_channel, Pair{Any, Bool}(value, false))
154 end
155 break
156 catch err
157 if lasterr !== nothing
158 println("SYSTEM ERROR: Failed to report error to REPL frontend")
159 println(err)
160 end
161 lasterr = current_exceptions()
162 end
163 end
164 Base.sigatomic_end()
165 nothing
166 end
167
168 function check_for_missing_packages_and_run_hooks(ast)
169 isa(ast, Expr) || return
170 mods = modules_to_be_loaded(ast)
171 filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
172 if !isempty(mods)
173 for f in install_packages_hooks
174 Base.invokelatest(f, mods) && return
175 end
176 end
177 end
178
179 function modules_to_be_loaded(ast::Expr, mods::Vector{Symbol} = Symbol[])
180 ast.head === :quote && return mods # don't search if it's not going to be run during this eval
181 if ast.head === :using || ast.head === :import
182 for arg in ast.args
183 arg = arg::Expr
184 arg1 = first(arg.args)
185 if arg1 isa Symbol # i.e. `Foo`
186 if arg1 != :. # don't include local imports
187 push!(mods, arg1)
188 end
189 else # i.e. `Foo: bar`
190 push!(mods, first((arg1::Expr).args))
191 end
192 end
193 end
194 for arg in ast.args
195 if isexpr(arg, (:block, :if, :using, :import))
196 modules_to_be_loaded(arg, mods)
197 end
198 end
199 filter!(mod -> !in(String(mod), ["Base", "Main", "Core"]), mods) # Exclude special non-package modules
200 return unique(mods)
201 end
202
203 """
204 start_repl_backend(repl_channel::Channel, response_channel::Channel)
205
206 Starts loop for REPL backend
207 Returns a REPLBackend with backend_task assigned
208
209 Deprecated since sync / async behavior cannot be selected
210 """
211 function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}
212 ; get_module::Function = ()->Main)
213 # Maintain legacy behavior of asynchronous backend
214 backend = REPLBackend(repl_channel, response_channel, false)
215 # Assignment will be made twice, but will be immediately available
216 backend.backend_task = @async start_repl_backend(backend; get_module)
217 return backend
218 end
219
220 """
221 start_repl_backend(backend::REPLBackend)
222
223 Call directly to run backend loop on current Task.
224 Use @async for run backend on new Task.
225
226 Does not return backend until loop is finished.
227 """
228 58 (100 %)
116 (200 %) samples spent in start_repl_backend
58 (50 %) (incl.) when called from #run_repl#59 line 389
58 (50 %) (incl.) when called from start_repl_backend line 228
58 (100 %) samples spent calling #start_repl_backend#46
function start_repl_backend(backend::REPLBackend, @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
229 backend.backend_task = Base.current_task()
230 consumer(backend)
231 58 (100 %)
58 (100 %) samples spent calling repl_backend_loop
repl_backend_loop(backend, get_module)
232 return backend
233 end
234
235
58 (100 %) samples spent in repl_backend_loop
58 (100 %) (incl.) when called from #start_repl_backend#46 line 231
function repl_backend_loop(backend::REPLBackend, get_module::Function)
236 # include looks at this to determine the relative include path
237 # nothing means cwd
238 while true
239 tls = task_local_storage()
240 tls[:SOURCE_PATH] = nothing
241 ast, show_value = take!(backend.repl_channel)
242 if show_value == -1
243 # exit flag
244 break
245 end
246 58 (100 %)
58 (100 %) samples spent calling eval_user_input
eval_user_input(ast, backend, get_module())
247 end
248 return nothing
249 end
250
251 struct REPLDisplay{R<:AbstractREPL} <: AbstractDisplay
252 repl::R
253 end
254
255 ==(a::REPLDisplay, b::REPLDisplay) = a.repl === b.repl
256
257 function display(d::REPLDisplay, mime::MIME"text/plain", x)
258 x = Ref{Any}(x)
259 with_repl_linfo(d.repl) do io
260 io = IOContext(io, :limit => true, :module => active_module(d)::Module)
261 if d.repl isa LineEditREPL
262 mistate = d.repl.mistate
263 mode = LineEdit.mode(mistate)
264 if mode isa LineEdit.Prompt
265 LineEdit.write_output_prefix(io, mode, get(io, :color, false)::Bool)
266 end
267 end
268 get(io, :color, false)::Bool && write(io, answer_color(d.repl))
269 if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
270 # this can override the :limit property set initially
271 io = foldl(IOContext, d.repl.options.iocontext, init=io)
272 end
273 show(io, mime, x[])
274 println(io)
275 end
276 return nothing
277 end
278 display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
279
280 function print_response(repl::AbstractREPL, response, show_value::Bool, have_color::Bool)
281 repl.waserror = response[2]
282 with_repl_linfo(repl) do io
283 io = IOContext(io, :module => active_module(repl)::Module)
284 print_response(io, response, show_value, have_color, specialdisplay(repl))
285 end
286 return nothing
287 end
288
289 function repl_display_error(errio::IO, @nospecialize errval)
290 # this will be set to true if types in the stacktrace are truncated
291 limitflag = Ref(false)
292 errio = IOContext(errio, :stacktrace_types_limited => limitflag)
293 Base.invokelatest(Base.display_error, errio, errval)
294 if limitflag[]
295 print(errio, "Some type information was truncated. Use `show(err)` to see complete types.")
296 println(errio)
297 end
298 return nothing
299 end
300
301 function print_response(errio::IO, response, show_value::Bool, have_color::Bool, specialdisplay::Union{AbstractDisplay,Nothing}=nothing)
302 Base.sigatomic_begin()
303 val, iserr = response
304 while true
305 try
306 Base.sigatomic_end()
307 if iserr
308 val = Base.scrub_repl_backtrace(val)
309 Base.istrivialerror(val) || setglobal!(Base.MainInclude, :err, val)
310 repl_display_error(errio, val)
311 else
312 if val !== nothing && show_value
313 try
314 if specialdisplay === nothing
315 Base.invokelatest(display, val)
316 else
317 Base.invokelatest(display, specialdisplay, val)
318 end
319 catch
320 println(errio, "Error showing value of type ", typeof(val), ":")
321 rethrow()
322 end
323 end
324 end
325 break
326 catch ex
327 if iserr
328 println(errio) # an error during printing is likely to leave us mid-line
329 println(errio, "SYSTEM (REPL): showing an error caused an error")
330 try
331 excs = Base.scrub_repl_backtrace(current_exceptions())
332 setglobal!(Base.MainInclude, :err, excs)
333 repl_display_error(errio, excs)
334 catch e
335 # at this point, only print the name of the type as a Symbol to
336 # minimize the possibility of further errors.
337 println(errio)
338 println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
339 " while trying to handle a nested exception; giving up")
340 end
341 break
342 end
343 val = current_exceptions()
344 iserr = true
345 end
346 end
347 Base.sigatomic_end()
348 nothing
349 end
350
351 # A reference to a backend that is not mutable
352 struct REPLBackendRef
353 repl_channel::Channel{Any}
354 response_channel::Channel{Any}
355 end
356 REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)
357
358 function destroy(ref::REPLBackendRef, state::Task)
359 if istaskfailed(state)
360 close(ref.repl_channel, TaskFailedException(state))
361 close(ref.response_channel, TaskFailedException(state))
362 end
363 close(ref.repl_channel)
364 close(ref.response_channel)
365 end
366
367 """
368 run_repl(repl::AbstractREPL)
369 run_repl(repl, consumer = backend->nothing; backend_on_current_task = true)
370
371 Main function to start the REPL
372
373 consumer is an optional function that takes a REPLBackend as an argument
374 """
375 58 (100 %)
116 (200 %) samples spent in run_repl
58 (50 %) (incl.) when called from #1013 line 432
58 (50 %) (incl.) when called from run_repl line 375
58 (100 %) samples spent calling #run_repl#59
function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); backend_on_current_task::Bool = true, backend = REPLBackend())
376 backend_ref = REPLBackendRef(backend)
377 cleanup = @task try
378 destroy(backend_ref, t)
379 catch e
380 Core.print(Core.stderr, "\nINTERNAL ERROR: ")
381 Core.println(Core.stderr, e)
382 Core.println(Core.stderr, catch_backtrace())
383 end
384 get_module = () -> active_module(repl)
385 if backend_on_current_task
386 t = @async run_frontend(repl, backend_ref)
387 errormonitor(t)
388 Base._wait2(t, cleanup)
389 58 (100 %)
58 (100 %) samples spent calling start_repl_backend
start_repl_backend(backend, consumer; get_module)
390 else
391 t = @async start_repl_backend(backend, consumer; get_module)
392 errormonitor(t)
393 Base._wait2(t, cleanup)
394 run_frontend(repl, backend_ref)
395 end
396 return backend
397 end
398
399 ## BasicREPL ##
400
401 mutable struct BasicREPL <: AbstractREPL
402 terminal::TextTerminal
403 waserror::Bool
404 frontend_task::Task
405 BasicREPL(t) = new(t, false)
406 end
407
408 outstream(r::BasicREPL) = r.terminal
409 hascolor(r::BasicREPL) = hascolor(r.terminal)
410
411 function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
412 repl.frontend_task = current_task()
413 d = REPLDisplay(repl)
414 dopushdisplay = !in(d,Base.Multimedia.displays)
415 dopushdisplay && pushdisplay(d)
416 hit_eof = false
417 while true
418 Base.reseteof(repl.terminal)
419 write(repl.terminal, JULIA_PROMPT)
420 line = ""
421 ast = nothing
422 interrupted = false
423 while true
424 try
425 line *= readline(repl.terminal, keep=true)
426 catch e
427 if isa(e,InterruptException)
428 try # raise the debugger if present
429 ccall(:jl_raise_debugger, Int, ())
430 catch
431 end
432 line = ""
433 interrupted = true
434 break
435 elseif isa(e,EOFError)
436 hit_eof = true
437 break
438 else
439 rethrow()
440 end
441 end
442 ast = Base.parse_input_line(line)
443 (isa(ast,Expr) && ast.head === :incomplete) || break
444 end
445 if !isempty(line)
446 response = eval_with_backend(ast, backend)
447 print_response(repl, response, !ends_with_semicolon(line), false)
448 end
449 write(repl.terminal, '\n')
450 ((!interrupted && isempty(line)) || hit_eof) && break
451 end
452 # terminate backend
453 put!(backend.repl_channel, (nothing, -1))
454 dopushdisplay && popdisplay(d)
455 nothing
456 end
457
458 ## LineEditREPL ##
459
460 mutable struct LineEditREPL <: AbstractREPL
461 t::TextTerminal
462 hascolor::Bool
463 prompt_color::String
464 input_color::String
465 answer_color::String
466 shell_color::String
467 help_color::String
468 history_file::Bool
469 in_shell::Bool
470 in_help::Bool
471 envcolors::Bool
472 waserror::Bool
473 specialdisplay::Union{Nothing,AbstractDisplay}
474 options::Options
475 mistate::Union{MIState,Nothing}
476 last_shown_line_infos::Vector{Tuple{String,Int}}
477 interface::ModalInterface
478 backendref::REPLBackendRef
479 frontend_task::Task
480 function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,in_help,envcolors)
481 opts = Options()
482 opts.hascolor = hascolor
483 if !hascolor
484 opts.beep_colors = [""]
485 end
486 new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,
487 in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
488 end
489 end
490 outstream(r::LineEditREPL) = (t = r.t; t isa TTYTerminal ? t.out_stream : t)
491 specialdisplay(r::LineEditREPL) = r.specialdisplay
492 specialdisplay(r::AbstractREPL) = nothing
493 terminal(r::LineEditREPL) = r.t
494 hascolor(r::LineEditREPL) = r.hascolor
495
496 LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
497 LineEditREPL(t, hascolor,
498 hascolor ? Base.text_colors[:green] : "",
499 hascolor ? Base.input_color() : "",
500 hascolor ? Base.answer_color() : "",
501 hascolor ? Base.text_colors[:red] : "",
502 hascolor ? Base.text_colors[:yellow] : "",
503 false, false, false, envcolors
504 )
505
506 mutable struct REPLCompletionProvider <: CompletionProvider
507 modifiers::LineEdit.Modifiers
508 end
509 REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
510
511 mutable struct ShellCompletionProvider <: CompletionProvider end
512 struct LatexCompletions <: CompletionProvider end
513
514 function active_module() # this method is also called from Base
515 isdefined(Base, :active_repl) || return Main
516 return active_module(Base.active_repl::AbstractREPL)
517 end
518 active_module((; mistate)::LineEditREPL) = mistate === nothing ? Main : mistate.active_module
519 active_module(::AbstractREPL) = Main
520 active_module(d::REPLDisplay) = active_module(d.repl)
521
522 setmodifiers!(c::CompletionProvider, m::LineEdit.Modifiers) = nothing
523
524 setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m
525
526 """
527 activate(mod::Module=Main)
528
529 Set `mod` as the default contextual module in the REPL,
530 both for evaluating expressions and printing them.
531 """
532 function activate(mod::Module=Main)
533 mistate = (Base.active_repl::LineEditREPL).mistate
534 mistate === nothing && return nothing
535 mistate.active_module = mod
536 Base.load_InteractiveUtils(mod)
537 return nothing
538 end
539
540 beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
541
542 function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module)
543 partial = beforecursor(s.input_buffer)
544 full = LineEdit.input_string(s)
545 ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift)
546 c.modifiers = LineEdit.Modifiers()
547 return unique!(map(completion_text, ret)), partial[range], should_complete
548 end
549
550 function complete_line(c::ShellCompletionProvider, s::PromptState)
551 # First parse everything up to the current position
552 partial = beforecursor(s.input_buffer)
553 full = LineEdit.input_string(s)
554 ret, range, should_complete = shell_completions(full, lastindex(partial))
555 return unique!(map(completion_text, ret)), partial[range], should_complete
556 end
557
558 function complete_line(c::LatexCompletions, s)
559 partial = beforecursor(LineEdit.buffer(s))
560 full = LineEdit.input_string(s)::String
561 ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
562 return unique!(map(completion_text, ret)), partial[range], should_complete
563 end
564
565 with_repl_linfo(f, repl) = f(outstream(repl))
566 function with_repl_linfo(f, repl::LineEditREPL)
567 linfos = Tuple{String,Int}[]
568 io = IOContext(outstream(repl), :last_shown_line_infos => linfos)
569 f(io)
570 if !isempty(linfos)
571 repl.last_shown_line_infos = linfos
572 end
573 nothing
574 end
575
576 mutable struct REPLHistoryProvider <: HistoryProvider
577 history::Vector{String}
578 file_path::String
579 history_file::Union{Nothing,IO}
580 start_idx::Int
581 cur_idx::Int
582 last_idx::Int
583 last_buffer::IOBuffer
584 last_mode::Union{Nothing,Prompt}
585 mode_mapping::Dict{Symbol,Prompt}
586 modes::Vector{Symbol}
587 end
588 REPLHistoryProvider(mode_mapping::Dict{Symbol}) =
589 REPLHistoryProvider(String[], "", nothing, 0, 0, -1, IOBuffer(),
590 nothing, mode_mapping, UInt8[])
591
592 invalid_history_message(path::String) = """
593 Invalid history file ($path) format:
594 If you have a history file left over from an older version of Julia,
595 try renaming or deleting it.
596 Invalid character: """
597
598 munged_history_message(path::String) = """
599 Invalid history file ($path) format:
600 An editor may have converted tabs to spaces at line """
601
602 function hist_open_file(hp::REPLHistoryProvider)
603 f = open(hp.file_path, read=true, write=true, create=true)
604 hp.history_file = f
605 seekend(f)
606 end
607
608 function hist_from_file(hp::REPLHistoryProvider, path::String)
609 getline(lines, i) = i > length(lines) ? "" : lines[i]
610 file_lines = readlines(path)
611 countlines = 0
612 while true
613 # First parse the metadata that starts with '#' in particular the REPL mode
614 countlines += 1
615 line = getline(file_lines, countlines)
616 mode = :julia
617 isempty(line) && break
618 line[1] != '#' &&
619 error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
620 while !isempty(line)
621 startswith(line, '#') || break
622 if startswith(line, "# mode: ")
623 mode = Symbol(SubString(line, 9))
624 end
625 countlines += 1
626 line = getline(file_lines, countlines)
627 end
628 isempty(line) && break
629
630 # Now parse the code for the current REPL mode
631 line[1] == ' ' &&
632 error(munged_history_message(path), countlines)
633 line[1] != '\t' &&
634 error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
635 lines = String[]
636 while !isempty(line)
637 push!(lines, chomp(SubString(line, 2)))
638 next_line = getline(file_lines, countlines+1)
639 isempty(next_line) && break
640 first(next_line) == ' ' && error(munged_history_message(path), countlines)
641 # A line not starting with a tab means we are done with code for this entry
642 first(next_line) != '\t' && break
643 countlines += 1
644 line = getline(file_lines, countlines)
645 end
646 push!(hp.modes, mode)
647 push!(hp.history, join(lines, '\n'))
648 end
649 hp.start_idx = length(hp.history)
650 return hp
651 end
652
653 function add_history(hist::REPLHistoryProvider, s::PromptState)
654 str = rstrip(String(take!(copy(s.input_buffer))))
655 isempty(strip(str)) && return
656 mode = mode_idx(hist, LineEdit.mode(s))
657 !isempty(hist.history) &&
658 isequal(mode, hist.modes[end]) && str == hist.history[end] && return
659 push!(hist.modes, mode)
660 push!(hist.history, str)
661 hist.history_file === nothing && return
662 entry = """
663 # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
664 # mode: $mode
665 $(replace(str, r"^"ms => "\t"))
666 """
667 # TODO: write-lock history file
668 try
669 seekend(hist.history_file)
670 catch err
671 (err isa SystemError) || rethrow()
672 # File handle might get stale after a while, especially under network file systems
673 # If this doesn't fix it (e.g. when file is deleted), we'll end up rethrowing anyway
674 hist_open_file(hist)
675 end
676 print(hist.history_file, entry)
677 flush(hist.history_file)
678 nothing
679 end
680
681 function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
682 max_idx = length(hist.history) + 1
683 @assert 1 <= hist.cur_idx <= max_idx
684 (1 <= idx <= max_idx) || return :none
685 idx != hist.cur_idx || return :none
686
687 # save the current line
688 if save_idx == max_idx
689 hist.last_mode = LineEdit.mode(s)
690 hist.last_buffer = copy(LineEdit.buffer(s))
691 else
692 hist.history[save_idx] = LineEdit.input_string(s)
693 hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
694 end
695
696 # load the saved line
697 if idx == max_idx
698 last_buffer = hist.last_buffer
699 LineEdit.transition(s, hist.last_mode) do
700 LineEdit.replace_line(s, last_buffer)
701 end
702 hist.last_mode = nothing
703 hist.last_buffer = IOBuffer()
704 else
705 if haskey(hist.mode_mapping, hist.modes[idx])
706 LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
707 LineEdit.replace_line(s, hist.history[idx])
708 end
709 else
710 return :skip
711 end
712 end
713 hist.cur_idx = idx
714
715 return :ok
716 end
717
718 # REPL History can also transitions modes
719 function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
720 if 1 <= hist.cur_idx <= length(hist.modes)
721 return hist.mode_mapping[hist.modes[hist.cur_idx]]
722 end
723 return nothing
724 end
725
726 function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
727 num::Int=1, save_idx::Int = hist.cur_idx)
728 num <= 0 && return history_next(s, hist, -num, save_idx)
729 hist.last_idx = -1
730 m = history_move(s, hist, hist.cur_idx-num, save_idx)
731 if m === :ok
732 LineEdit.move_input_start(s)
733 LineEdit.reset_key_repeats(s) do
734 LineEdit.move_line_end(s)
735 end
736 return LineEdit.refresh_line(s)
737 elseif m === :skip
738 return history_prev(s, hist, num+1, save_idx)
739 else
740 return Terminals.beep(s)
741 end
742 end
743
744 function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
745 num::Int=1, save_idx::Int = hist.cur_idx)
746 if num == 0
747 Terminals.beep(s)
748 return
749 end
750 num < 0 && return history_prev(s, hist, -num, save_idx)
751 cur_idx = hist.cur_idx
752 max_idx = length(hist.history) + 1
753 if cur_idx == max_idx && 0 < hist.last_idx
754 # issue #6312
755 cur_idx = hist.last_idx
756 hist.last_idx = -1
757 end
758 m = history_move(s, hist, cur_idx+num, save_idx)
759 if m === :ok
760 LineEdit.move_input_end(s)
761 return LineEdit.refresh_line(s)
762 elseif m === :skip
763 return history_next(s, hist, num+1, save_idx)
764 else
765 return Terminals.beep(s)
766 end
767 end
768
769 history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
770 history_prev(s, hist, hist.cur_idx - 1 -
771 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
772
773 history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
774 history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
775
776 function history_move_prefix(s::LineEdit.PrefixSearchState,
777 hist::REPLHistoryProvider,
778 prefix::AbstractString,
779 backwards::Bool,
780 cur_idx::Int = hist.cur_idx)
781 cur_response = String(take!(copy(LineEdit.buffer(s))))
782 # when searching forward, start at last_idx
783 if !backwards && hist.last_idx > 0
784 cur_idx = hist.last_idx
785 end
786 hist.last_idx = -1
787 max_idx = length(hist.history)+1
788 idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):1:max_idx)
789 for idx in idxs
790 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)))
791 m = history_move(s, hist, idx)
792 if m === :ok
793 if idx == max_idx
794 # on resuming the in-progress edit, leave the cursor where the user last had it
795 elseif isempty(prefix)
796 # on empty prefix search, move cursor to the end
797 LineEdit.move_input_end(s)
798 else
799 # otherwise, keep cursor at the prefix position as a visual cue
800 seek(LineEdit.buffer(s), sizeof(prefix))
801 end
802 LineEdit.refresh_line(s)
803 return :ok
804 elseif m === :skip
805 return history_move_prefix(s,hist,prefix,backwards,idx)
806 end
807 end
808 end
809 Terminals.beep(s)
810 nothing
811 end
812 history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
813 history_move_prefix(s, hist, prefix, false)
814 history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
815 history_move_prefix(s, hist, prefix, true)
816
817 function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
818 backwards::Bool=false, skip_current::Bool=false)
819
820 qpos = position(query_buffer)
821 qpos > 0 || return true
822 searchdata = beforecursor(query_buffer)
823 response_str = String(take!(copy(response_buffer)))
824
825 # Alright, first try to see if the current match still works
826 a = position(response_buffer) + 1 # position is zero-indexed
827 # FIXME: I'm pretty sure this is broken since it uses an index
828 # into the search data to index into the response string
829 b = a + sizeof(searchdata)
830 b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
831 b = min(lastindex(response_str), b) # ensure that b is valid
832
833 searchstart = backwards ? b : a
834 if searchdata == response_str[a:b]
835 if skip_current
836 searchstart = backwards ? prevind(response_str, b) : nextind(response_str, a)
837 else
838 return true
839 end
840 end
841
842 # Start searching
843 # First the current response buffer
844 if 1 <= searchstart <= lastindex(response_str)
845 match = backwards ? findprev(searchdata, response_str, searchstart) :
846 findnext(searchdata, response_str, searchstart)
847 if match !== nothing
848 seek(response_buffer, first(match) - 1)
849 return true
850 end
851 end
852
853 # Now search all the other buffers
854 idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):1:length(hist.history))
855 for idx in idxs
856 h = hist.history[idx]
857 match = backwards ? findlast(searchdata, h) : findfirst(searchdata, h)
858 if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
859 truncate(response_buffer, 0)
860 write(response_buffer, h)
861 seek(response_buffer, first(match) - 1)
862 hist.cur_idx = idx
863 return true
864 end
865 end
866
867 return false
868 end
869
870 function history_reset_state(hist::REPLHistoryProvider)
871 if hist.cur_idx != length(hist.history) + 1
872 hist.last_idx = hist.cur_idx
873 hist.cur_idx = length(hist.history) + 1
874 end
875 nothing
876 end
877 LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
878
879 function return_callback(s)
880 ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
881 return !(isa(ast, Expr) && ast.head === :incomplete)
882 end
883
884 find_hist_file() = get(ENV, "JULIA_HISTORY",
885 !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
886 error("DEPOT_PATH is empty and and ENV[\"JULIA_HISTORY\"] not set."))
887
888 backend(r::AbstractREPL) = r.backendref
889
890 function eval_with_backend(ast, backend::REPLBackendRef)
891 put!(backend.repl_channel, (ast, 1))
892 return take!(backend.response_channel) # (val, iserr)
893 end
894
895 function respond(f, repl, main; pass_empty::Bool = false, suppress_on_semicolon::Bool = true)
896 return function do_respond(s::MIState, buf, ok::Bool)
897 if !ok
898 return transition(s, :abort)
899 end
900 line = String(take!(buf)::Vector{UInt8})
901 if !isempty(line) || pass_empty
902 reset(repl)
903 local response
904 try
905 ast = Base.invokelatest(f, line)
906 response = eval_with_backend(ast, backend(repl))
907 catch
908 response = Pair{Any, Bool}(current_exceptions(), true)
909 end
910 hide_output = suppress_on_semicolon && ends_with_semicolon(line)
911 print_response(repl, response, !hide_output, hascolor(repl))
912 end
913 prepare_next(repl)
914 reset_state(s)
915 return s.current_mode.sticky ? true : transition(s, main)
916 end
917 end
918
919 function reset(repl::LineEditREPL)
920 raw!(repl.t, false)
921 hascolor(repl) && print(repl.t, Base.text_colors[:normal])
922 nothing
923 end
924
925 function prepare_next(repl::LineEditREPL)
926 println(terminal(repl))
927 end
928
929 function mode_keymap(julia_prompt::Prompt)
930 AnyDict(
931 '\b' => function (s::MIState,o...)
932 if isempty(s) || position(LineEdit.buffer(s)) == 0
933 buf = copy(LineEdit.buffer(s))
934 transition(s, julia_prompt) do
935 LineEdit.state(s, julia_prompt).input_buffer = buf
936 end
937 else
938 LineEdit.edit_backspace(s)
939 end
940 end,
941 "^C" => function (s::MIState,o...)
942 LineEdit.move_input_end(s)
943 LineEdit.refresh_line(s)
944 print(LineEdit.terminal(s), "^C\n\n")
945 transition(s, julia_prompt)
946 transition(s, :reset)
947 LineEdit.refresh_line(s)
948 end)
949 end
950
951 repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
952 repl_filename(repl, hp) = "REPL"
953
954 const JL_PROMPT_PASTE = Ref(true)
955 enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
956
957 function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
958 function ()
959 mod = active_module(repl)
960 prefix = mod == Main ? "" : string('(', mod, ") ")
961 pr = prompt isa String ? prompt : prompt()
962 prefix * pr
963 end
964 end
965
966 setup_interface(
967 repl::LineEditREPL;
968 # those keyword arguments may be deprecated eventually in favor of the Options mechanism
969 hascolor::Bool = repl.options.hascolor,
970 extra_repl_keymap::Any = repl.options.extra_keymap
971 ) = setup_interface(repl, hascolor, extra_repl_keymap)
972
973 # This non keyword method can be precompiled which is important
974 function setup_interface(
975 repl::LineEditREPL,
976 hascolor::Bool,
977 extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
978 )
979 # The precompile statement emitter has problem outputting valid syntax for the
980 # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
981 # This function is however important to precompile for REPL startup time, therefore,
982 # make the type Any and just assert that we have the correct type below.
983 @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
984
985 ###
986 #
987 # This function returns the main interface that describes the REPL
988 # functionality, it is called internally by functions that setup a
989 # Terminal-based REPL frontend.
990 #
991 # See run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
992 # for usage
993 #
994 ###
995
996 ###
997 # We setup the interface in two stages.
998 # First, we set up all components (prompt,rsearch,shell,help)
999 # Second, we create keymaps with appropriate transitions between them
1000 # and assign them to the components
1001 #
1002 ###
1003
1004 ############################### Stage I ################################
1005
1006 # This will provide completions for REPL and help mode
1007 replc = REPLCompletionProvider()
1008
1009 # Set up the main Julia prompt
1010 julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
1011 # Copy colors from the prompt object
1012 prompt_prefix = hascolor ? repl.prompt_color : "",
1013 prompt_suffix = hascolor ?
1014 (repl.envcolors ? Base.input_color : repl.input_color) : "",
1015 repl = repl,
1016 complete = replc,
1017 on_enter = return_callback)
1018
1019 # Setup help mode
1020 help_mode = Prompt(contextual_prompt(repl, "help?> "),
1021 prompt_prefix = hascolor ? repl.help_color : "",
1022 prompt_suffix = hascolor ?
1023 (repl.envcolors ? Base.input_color : repl.input_color) : "",
1024 repl = repl,
1025 complete = replc,
1026 # When we're done transform the entered line into a call to helpmode function
1027 on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
1028 repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
1029
1030
1031 # Set up shell mode
1032 shell_mode = Prompt(SHELL_PROMPT;
1033 prompt_prefix = hascolor ? repl.shell_color : "",
1034 prompt_suffix = hascolor ?
1035 (repl.envcolors ? Base.input_color : repl.input_color) : "",
1036 repl = repl,
1037 complete = ShellCompletionProvider(),
1038 # Transform "foo bar baz" into `foo bar baz` (shell quoting)
1039 # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
1040 # special)
1041 on_done = respond(repl, julia_prompt) do line
1042 Expr(:call, :(Base.repl_cmd),
1043 :(Base.cmd_gen($(Base.shell_parse(line::String)[1]))),
1044 outstream(repl))
1045 end,
1046 sticky = true)
1047
1048
1049 ################################# Stage II #############################
1050
1051 # Setup history
1052 # We will have a unified history for all REPL modes
1053 hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
1054 :shell => shell_mode,
1055 :help => help_mode))
1056 if repl.history_file
1057 try
1058 hist_path = find_hist_file()
1059 mkpath(dirname(hist_path))
1060 hp.file_path = hist_path
1061 hist_open_file(hp)
1062 finalizer(replc) do replc
1063 close(hp.history_file)
1064 end
1065 hist_from_file(hp, hist_path)
1066 catch
1067 # use REPL.hascolor to avoid using the local variable with the same name
1068 print_response(repl, Pair{Any, Bool}(current_exceptions(), true), true, REPL.hascolor(repl))
1069 println(outstream(repl))
1070 @info "Disabling history file for this session"
1071 repl.history_file = false
1072 end
1073 end
1074 history_reset_state(hp)
1075 julia_prompt.hist = hp
1076 shell_mode.hist = hp
1077 help_mode.hist = hp
1078
1079 julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
1080
1081
1082 search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
1083 search_prompt.complete = LatexCompletions()
1084
1085 shell_prompt_len = length(SHELL_PROMPT)
1086 help_prompt_len = length(HELP_PROMPT)
1087 jl_prompt_regex = r"^In \[[0-9]+\]: |^(?:\(.+\) )?julia> "
1088 pkg_prompt_regex = r"^(?:\(.+\) )?pkg> "
1089
1090 # Canonicalize user keymap input
1091 if isa(extra_repl_keymap, Dict)
1092 extra_repl_keymap = AnyDict[extra_repl_keymap]
1093 end
1094
1095 repl_keymap = AnyDict(
1096 ';' => function (s::MIState,o...)
1097 if isempty(s) || position(LineEdit.buffer(s)) == 0
1098 buf = copy(LineEdit.buffer(s))
1099 transition(s, shell_mode) do
1100 LineEdit.state(s, shell_mode).input_buffer = buf
1101 end
1102 else
1103 edit_insert(s, ';')
1104 end
1105 end,
1106 '?' => function (s::MIState,o...)
1107 if isempty(s) || position(LineEdit.buffer(s)) == 0
1108 buf = copy(LineEdit.buffer(s))
1109 transition(s, help_mode) do
1110 LineEdit.state(s, help_mode).input_buffer = buf
1111 end
1112 else
1113 edit_insert(s, '?')
1114 end
1115 end,
1116
1117 # Bracketed Paste Mode
1118 "\e[200~" => (s::MIState,o...)->begin
1119 input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
1120 sbuffer = LineEdit.buffer(s)
1121 curspos = position(sbuffer)
1122 seek(sbuffer, 0)
1123 shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
1124 seek(sbuffer, curspos)
1125 if curspos == 0
1126 # if pasting at the beginning, strip leading whitespace
1127 input = lstrip(input)
1128 end
1129 if !shouldeval
1130 # when pasting in the middle of input, just paste in place
1131 # don't try to execute all the WIP, since that's rather confusing
1132 # and is often ill-defined how it should behave
1133 edit_insert(s, input)
1134 return
1135 end
1136 LineEdit.push_undo(s)
1137 edit_insert(sbuffer, input)
1138 input = String(take!(sbuffer))
1139 oldpos = firstindex(input)
1140 firstline = true
1141 isprompt_paste = false
1142 curr_prompt_len = 0
1143 pasting_help = false
1144
1145 while oldpos <= lastindex(input) # loop until all lines have been executed
1146 if JL_PROMPT_PASTE[]
1147 # Check if the next statement starts with a prompt i.e. "julia> ", in that case
1148 # skip it. But first skip whitespace unless pasting in a docstring which may have
1149 # indented prompt examples that we don't want to execute
1150 while input[oldpos] in (pasting_help ? ('\n') : ('\n', ' ', '\t'))
1151 oldpos = nextind(input, oldpos)
1152 oldpos >= sizeof(input) && return
1153 end
1154 substr = SubString(input, oldpos)
1155 # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
1156 if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex)
1157 detected_jl_prompt = match(jl_prompt_regex, substr).match
1158 isprompt_paste = true
1159 curr_prompt_len = sizeof(detected_jl_prompt)
1160 oldpos += curr_prompt_len
1161 transition(s, julia_prompt)
1162 pasting_help = false
1163 # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode
1164 elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex)
1165 detected_pkg_prompt = match(pkg_prompt_regex, substr).match
1166 isprompt_paste = true
1167 curr_prompt_len = sizeof(detected_pkg_prompt)
1168 oldpos += curr_prompt_len
1169 Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...)
1170 pasting_help = false
1171 # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode
1172 elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT)
1173 isprompt_paste = true
1174 oldpos += shell_prompt_len
1175 curr_prompt_len = shell_prompt_len
1176 transition(s, shell_mode)
1177 pasting_help = false
1178 # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode
1179 elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT)
1180 isprompt_paste = true
1181 oldpos += help_prompt_len
1182 curr_prompt_len = help_prompt_len
1183 transition(s, help_mode)
1184 pasting_help = true
1185 # If we are prompt pasting and current statement does not begin with a mode prefix, skip to next line
1186 elseif isprompt_paste
1187 while input[oldpos] != '\n'
1188 oldpos = nextind(input, oldpos)
1189 oldpos >= sizeof(input) && return
1190 end
1191 continue
1192 end
1193 end
1194 dump_tail = false
1195 nl_pos = findfirst('\n', input[oldpos:end])
1196 if s.current_mode == julia_prompt
1197 ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
1198 if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
1199 (pos > ncodeunits(input) && !endswith(input, '\n'))
1200 # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1201 # Insert all the remaining text as one line (might be empty)
1202 dump_tail = true
1203 end
1204 elseif isnothing(nl_pos) # no newline at end, so just dump the tail into the prompt and don't execute
1205 dump_tail = true
1206 elseif s.current_mode == shell_mode # handle multiline shell commands
1207 lines = split(input[oldpos:end], '\n')
1208 pos = oldpos + sizeof(lines[1]) + 1
1209 if length(lines) > 1
1210 for line in lines[2:end]
1211 # to be recognized as a multiline shell command, the lines must be indented to the
1212 # same prompt position
1213 if !startswith(line, ' '^curr_prompt_len)
1214 break
1215 end
1216 pos += sizeof(line) + 1
1217 end
1218 end
1219 else
1220 pos = oldpos + nl_pos
1221 end
1222 if dump_tail
1223 tail = input[oldpos:end]
1224 if !firstline
1225 # strip leading whitespace, but only if it was the result of executing something
1226 # (avoids modifying the user's current leading wip line)
1227 tail = lstrip(tail)
1228 end
1229 if isprompt_paste # remove indentation spaces corresponding to the prompt
1230 tail = replace(tail, r"^"m * ' '^curr_prompt_len => "")
1231 end
1232 LineEdit.replace_line(s, tail, true)
1233 LineEdit.refresh_line(s)
1234 break
1235 end
1236 # get the line and strip leading and trailing whitespace
1237 line = strip(input[oldpos:prevind(input, pos)])
1238 if !isempty(line)
1239 if isprompt_paste # remove indentation spaces corresponding to the prompt
1240 line = replace(line, r"^"m * ' '^curr_prompt_len => "")
1241 end
1242 # put the line on the screen and history
1243 LineEdit.replace_line(s, line)
1244 LineEdit.commit_line(s)
1245 # execute the statement
1246 terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
1247 raw!(terminal, false) && disable_bracketed_paste(terminal)
1248 LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
1249 raw!(terminal, true) && enable_bracketed_paste(terminal)
1250 LineEdit.push_undo(s) # when the last line is incomplete
1251 end
1252 oldpos = pos
1253 firstline = false
1254 end
1255 end,
1256
1257 # Open the editor at the location of a stackframe or method
1258 # This is accessing a contextual variable that gets set in
1259 # the show_backtrace and show_method_table functions.
1260 "^Q" => (s::MIState, o...) -> begin
1261 linfos = repl.last_shown_line_infos
1262 str = String(take!(LineEdit.buffer(s)))
1263 n = tryparse(Int, str)
1264 n === nothing && @goto writeback
1265 if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "REPL[")
1266 @goto writeback
1267 end
1268 try
1269 InteractiveUtils.edit(Base.fixup_stdlib_path(linfos[n][1]), linfos[n][2])
1270 catch ex
1271 ex isa ProcessFailedException || ex isa Base.IOError || ex isa SystemError || rethrow()
1272 @info "edit failed" _exception=ex
1273 end
1274 LineEdit.refresh_line(s)
1275 return
1276 @label writeback
1277 write(LineEdit.buffer(s), str)
1278 return
1279 end,
1280 )
1281
1282 prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
1283
1284 a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1285 prepend!(a, extra_repl_keymap)
1286
1287 julia_prompt.keymap_dict = LineEdit.keymap(a)
1288
1289 mk = mode_keymap(julia_prompt)
1290
1291 b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1292 prepend!(b, extra_repl_keymap)
1293
1294 shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
1295
1296 allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt]
1297 return ModalInterface(allprompts)
1298 end
1299
1300 function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1301 repl.frontend_task = current_task()
1302 d = REPLDisplay(repl)
1303 dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
1304 dopushdisplay && pushdisplay(d)
1305 if !isdefined(repl,:interface)
1306 interface = repl.interface = setup_interface(repl)
1307 else
1308 interface = repl.interface
1309 end
1310 repl.backendref = backend
1311 repl.mistate = LineEdit.init_state(terminal(repl), interface)
1312 run_interface(terminal(repl), interface, repl.mistate)
1313 # Terminate Backend
1314 put!(backend.repl_channel, (nothing, -1))
1315 dopushdisplay && popdisplay(d)
1316 nothing
1317 end
1318
1319 ## StreamREPL ##
1320
1321 mutable struct StreamREPL <: AbstractREPL
1322 stream::IO
1323 prompt_color::String
1324 input_color::String
1325 answer_color::String
1326 waserror::Bool
1327 frontend_task::Task
1328 StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
1329 end
1330 StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
1331 run_repl(stream::IO) = run_repl(StreamREPL(stream))
1332
1333 outstream(s::StreamREPL) = s.stream
1334 hascolor(s::StreamREPL) = get(s.stream, :color, false)::Bool
1335
1336 answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
1337 answer_color(r::StreamREPL) = r.answer_color
1338 input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
1339 input_color(r::StreamREPL) = r.input_color
1340
1341 let matchend = Dict("\"" => r"\"", "\"\"\"" => r"\"\"\"", "'" => r"'",
1342 "`" => r"`", "```" => r"```", "#" => r"$"m, "#=" => r"=#|#=")
1343 global _rm_strings_and_comments
1344 function _rm_strings_and_comments(code::Union{String,SubString{String}})
1345 buf = IOBuffer(sizehint = sizeof(code))
1346 pos = 1
1347 while true
1348 i = findnext(r"\"(?!\"\")|\"\"\"|'|`(?!``)|```|#(?!=)|#=", code, pos)
1349 isnothing(i) && break
1350 match = SubString(code, i)
1351 j = findnext(matchend[match]::Regex, code, nextind(code, last(i)))
1352 if match == "#=" # possibly nested
1353 nested = 1
1354 while j !== nothing
1355 nested += SubString(code, j) == "#=" ? +1 : -1
1356 iszero(nested) && break
1357 j = findnext(r"=#|#=", code, nextind(code, last(j)))
1358 end
1359 elseif match[1] != '#' # quote match: check non-escaped
1360 while j !== nothing
1361 notbackslash = findprev(!=('\\'), code, prevind(code, first(j)))::Int
1362 isodd(first(j) - notbackslash) && break # not escaped
1363 j = findnext(matchend[match]::Regex, code, nextind(code, first(j)))
1364 end
1365 end
1366 isnothing(j) && break
1367 if match[1] == '#'
1368 print(buf, SubString(code, pos, prevind(code, first(i))))
1369 else
1370 print(buf, SubString(code, pos, last(i)), ' ', SubString(code, j))
1371 end
1372 pos = nextind(code, last(j))
1373 end
1374 print(buf, SubString(code, pos, lastindex(code)))
1375 return String(take!(buf))
1376 end
1377 end
1378
1379 # heuristic function to decide if the presence of a semicolon
1380 # at the end of the expression was intended for suppressing output
1381 ends_with_semicolon(code::AbstractString) = ends_with_semicolon(String(code))
1382 ends_with_semicolon(code::Union{String,SubString{String}}) =
1383 contains(_rm_strings_and_comments(code), r";\s*$")
1384
1385 function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
1386 repl.frontend_task = current_task()
1387 have_color = hascolor(repl)
1388 Base.banner(repl.stream)
1389 d = REPLDisplay(repl)
1390 dopushdisplay = !in(d,Base.Multimedia.displays)
1391 dopushdisplay && pushdisplay(d)
1392 while !eof(repl.stream)::Bool
1393 if have_color
1394 print(repl.stream,repl.prompt_color)
1395 end
1396 print(repl.stream, "julia> ")
1397 if have_color
1398 print(repl.stream, input_color(repl))
1399 end
1400 line = readline(repl.stream, keep=true)
1401 if !isempty(line)
1402 ast = Base.parse_input_line(line)
1403 if have_color
1404 print(repl.stream, Base.color_normal)
1405 end
1406 response = eval_with_backend(ast, backend)
1407 print_response(repl, response, !ends_with_semicolon(line), have_color)
1408 end
1409 end
1410 # Terminate Backend
1411 put!(backend.repl_channel, (nothing, -1))
1412 dopushdisplay && popdisplay(d)
1413 nothing
1414 end
1415
1416 module Numbered
1417
1418 using ..REPL
1419
1420 __current_ast_transforms() = isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
1421
1422 function repl_eval_counter(hp)
1423 return length(hp.history) - hp.start_idx
1424 end
1425
1426 function out_transform(@nospecialize(x), n::Ref{Int})
1427 return Expr(:toplevel, get_usings!([], x)..., quote
1428 let __temp_val_a72df459 = $x
1429 $capture_result($n, __temp_val_a72df459)
1430 __temp_val_a72df459
1431 end
1432 end)
1433 end
1434
1435 function get_usings!(usings, ex)
1436 ex isa Expr || return usings
1437 # get all `using` and `import` statements which are at the top level
1438 for (i, arg) in enumerate(ex.args)
1439 if Base.isexpr(arg, :toplevel)
1440 get_usings!(usings, arg)
1441 elseif Base.isexpr(arg, [:using, :import])
1442 push!(usings, popat!(ex.args, i))
1443 end
1444 end
1445 return usings
1446 end
1447
1448 function capture_result(n::Ref{Int}, @nospecialize(x))
1449 n = n[]
1450 mod = Base.MainInclude
1451 if !isdefined(mod, :Out)
1452 @eval mod global Out
1453 @eval mod export Out
1454 setglobal!(mod, :Out, Dict{Int, Any}())
1455 end
1456 if x !== getglobal(mod, :Out) && x !== nothing # remove this?
1457 getglobal(mod, :Out)[n] = x
1458 end
1459 nothing
1460 end
1461
1462 function set_prompt(repl::LineEditREPL, n::Ref{Int})
1463 julia_prompt = repl.interface.modes[1]
1464 julia_prompt.prompt = function()
1465 n[] = repl_eval_counter(julia_prompt.hist)+1
1466 string("In [", n[], "]: ")
1467 end
1468 nothing
1469 end
1470
1471 function set_output_prefix(repl::LineEditREPL, n::Ref{Int})
1472 julia_prompt = repl.interface.modes[1]
1473 if REPL.hascolor(repl)
1474 julia_prompt.output_prefix_prefix = Base.text_colors[:red]
1475 end
1476 julia_prompt.output_prefix = () -> string("Out[", n[], "]: ")
1477 nothing
1478 end
1479
1480 function __current_ast_transforms(backend)
1481 if backend === nothing
1482 isdefined(Base, :active_repl_backend) ? Base.active_repl_backend.ast_transforms : REPL.repl_ast_transforms
1483 else
1484 backend.ast_transforms
1485 end
1486 end
1487
1488
1489 function numbered_prompt!(repl::LineEditREPL=Base.active_repl, backend=nothing)
1490 n = Ref{Int}(0)
1491 set_prompt(repl, n)
1492 set_output_prefix(repl, n)
1493 push!(__current_ast_transforms(backend), @nospecialize(ast) -> out_transform(ast, n))
1494 return
1495 end
1496
1497 """
1498 Out[n]
1499
1500 A variable referring to all previously computed values, automatically imported to the interactive prompt.
1501 Only defined and exists while using [Numbered prompt](@ref Numbered-prompt).
1502
1503 See also [`ans`](@ref).
1504 """
1505 Base.MainInclude.Out
1506
1507 end
1508
1509 import .Numbered.numbered_prompt!
1510
1511 end # module