StatProfilerHTML.jl report
Generated on Thu, 26 Mar 2020 19:09:01
File source code
Line Exclusive Inclusive Code
1 # This file is a part of Julia. License is MIT: https://julialang.org/license
2
3 module REPL
4
5 using Base.Meta, Sockets
6 import InteractiveUtils
7
8 export
9 AbstractREPL,
10 BasicREPL,
11 LineEditREPL,
12 StreamREPL
13
14 import Base:
15 AbstractDisplay,
16 display,
17 show,
18 AnyDict,
19 ==,
20 catch_stack
21
22
23 include("Terminals.jl")
24 using .Terminals
25
26 include("LineEdit.jl")
27 using .LineEdit
28 import ..LineEdit:
29 CompletionProvider,
30 HistoryProvider,
31 add_history,
32 complete_line,
33 history_next,
34 history_next_prefix,
35 history_prev,
36 history_prev_prefix,
37 history_first,
38 history_last,
39 history_search,
40 accept_result,
41 terminal,
42 MIState
43
44 include("REPLCompletions.jl")
45 using .REPLCompletions
46
47 include("TerminalMenus/TerminalMenus.jl")
48 include("docview.jl")
49
50 @nospecialize # use only declared type signatures
51
52 function __init__()
53 Base.REPL_MODULE_REF[] = REPL
54 end
55
56 abstract type AbstractREPL end
57
58 answer_color(::AbstractREPL) = ""
59
60 const JULIA_PROMPT = "julia> "
61
62 mutable struct REPLBackend
63 "channel for AST"
64 repl_channel::Channel
65 "channel for results: (value, iserror)"
66 response_channel::Channel
67 "flag indicating the state of this backend"
68 in_eval::Bool
69 "transformation functions to apply before evaluating expressions"
70 ast_transforms::Vector{Any}
71 "current backend task"
72 backend_task::Task
73
74 REPLBackend(repl_channel, response_channel, in_eval, ast_transforms=copy(repl_ast_transforms)) =
75 new(repl_channel, response_channel, in_eval, ast_transforms)
76 end
77
78 """
79 softscope(ex)
80
81 Return a modified version of the parsed expression `ex` that uses
82 the REPL's "soft" scoping rules for global syntax blocks.
83 """
84 function softscope(@nospecialize ex)
85 if ex isa Expr
86 h = ex.head
87 if h === :toplevel
88 ex′ = Expr(h)
89 map!(softscope, resize!(ex′.args, length(ex.args)), ex.args)
90 return ex′
91 elseif h in (:meta, :import, :using, :export, :module, :error, :incomplete, :thunk)
92 return ex
93 else
94 return Expr(:block, Expr(:softscope, true), ex)
95 end
96 end
97 return ex
98 end
99
100 # Temporary alias until Documenter updates
101 const softscope! = softscope
102
103 const repl_ast_transforms = Any[softscope] # defaults for new REPL backends
104
105
833 (100 %) samples spent in eval_user_input
833 (100 %) (incl.) when called from run_backend line 1070
function eval_user_input(@nospecialize(ast), backend::REPLBackend)
106 lasterr = nothing
107 Base.sigatomic_begin()
108 while true
109 try
110 Base.sigatomic_end()
111 if lasterr !== nothing
112 put!(backend.response_channel, (lasterr,true))
113 else
114 backend.in_eval = true
115 for xf in backend.ast_transforms
116 ast = Base.invokelatest(xf, ast)
117 end
118 833 (100 %)
833 (100 %) samples spent calling eval
value = Core.eval(Main, ast)
119 backend.in_eval = false
120 # note: use jl_set_global to make sure value isn't passed through `expand`
121 ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :ans, value)
122 put!(backend.response_channel, (value,false))
123 end
124 break
125 catch err
126 if lasterr !== nothing
127 println("SYSTEM ERROR: Failed to report error to REPL frontend")
128 println(err)
129 end
130 lasterr = catch_stack()
131 end
132 end
133 Base.sigatomic_end()
134 nothing
135 end
136
137 function start_repl_backend(repl_channel::Channel, response_channel::Channel)
138 backend = REPLBackend(repl_channel, response_channel, false)
139 backend.backend_task = @async begin
140 # include looks at this to determine the relative include path
141 # nothing means cwd
142 while true
143 tls = task_local_storage()
144 tls[:SOURCE_PATH] = nothing
145 ast, show_value = take!(backend.repl_channel)
146 if show_value == -1
147 # exit flag
148 break
149 end
150 eval_user_input(ast, backend)
151 end
152 end
153 return backend
154 end
155 struct REPLDisplay{R<:AbstractREPL} <: AbstractDisplay
156 repl::R
157 end
158
159 ==(a::REPLDisplay, b::REPLDisplay) = a.repl === b.repl
160
161 function display(d::REPLDisplay, mime::MIME"text/plain", x)
162 io = outstream(d.repl)
163 get(io, :color, false) && write(io, answer_color(d.repl))
164 if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
165 # this can override the :limit property set initially
166 io = foldl(IOContext, d.repl.options.iocontext,
167 init=IOContext(io, :limit => true, :module => Main))
168 end
169 show(io, mime, x)
170 println(io)
171 nothing
172 end
173 display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
174
175 function print_response(repl::AbstractREPL, @nospecialize(response), show_value::Bool, have_color::Bool)
176 repl.waserror = response[2]
177 io = IOContext(outstream(repl), :module => Main)
178 print_response(io, response, show_value, have_color, specialdisplay(repl))
179 nothing
180 end
181 function print_response(errio::IO, @nospecialize(response), show_value::Bool, have_color::Bool, specialdisplay=nothing)
182 Base.sigatomic_begin()
183 val, iserr = response
184 while true
185 try
186 Base.sigatomic_end()
187 if iserr
188 Base.invokelatest(Base.display_error, errio, val)
189 else
190 if val !== nothing && show_value
191 try
192 if specialdisplay === nothing
193 Base.invokelatest(display, val)
194 else
195 Base.invokelatest(display, specialdisplay, val)
196 end
197 catch
198 println(errio, "Error showing value of type ", typeof(val), ":")
199 rethrow()
200 end
201 end
202 end
203 break
204 catch
205 if iserr
206 println(errio) # an error during printing is likely to leave us mid-line
207 println(errio, "SYSTEM (REPL): showing an error caused an error")
208 try
209 Base.invokelatest(Base.display_error, errio, catch_stack())
210 catch e
211 # at this point, only print the name of the type as a Symbol to
212 # minimize the possibility of further errors.
213 println(errio)
214 println(errio, "SYSTEM (REPL): caught exception of type ", typeof(e).name.name,
215 " while trying to handle a nested exception; giving up")
216 end
217 break
218 end
219 val = catch_stack()
220 iserr = true
221 end
222 end
223 Base.sigatomic_end()
224 nothing
225 end
226
227 # A reference to a backend
228 struct REPLBackendRef
229 repl_channel::Channel
230 response_channel::Channel
231 end
232
233 function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing))
234 repl_channel = Channel(1)
235 response_channel = Channel(1)
236 backend = start_repl_backend(repl_channel, response_channel)
237 consumer(backend)
238 run_frontend(repl, REPLBackendRef(repl_channel, response_channel))
239 return backend
240 end
241
242 ## BasicREPL ##
243
244 mutable struct BasicREPL <: AbstractREPL
245 terminal::TextTerminal
246 waserror::Bool
247 BasicREPL(t) = new(t, false)
248 end
249
250 outstream(r::BasicREPL) = r.terminal
251
252 function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
253 d = REPLDisplay(repl)
254 dopushdisplay = !in(d,Base.Multimedia.displays)
255 dopushdisplay && pushdisplay(d)
256 hit_eof = false
257 while true
258 Base.reseteof(repl.terminal)
259 write(repl.terminal, JULIA_PROMPT)
260 line = ""
261 ast = nothing
262 interrupted = false
263 while true
264 try
265 line *= readline(repl.terminal, keep=true)
266 catch e
267 if isa(e,InterruptException)
268 try # raise the debugger if present
269 ccall(:jl_raise_debugger, Int, ())
270 catch
271 end
272 line = ""
273 interrupted = true
274 break
275 elseif isa(e,EOFError)
276 hit_eof = true
277 break
278 else
279 rethrow()
280 end
281 end
282 ast = Base.parse_input_line(line)
283 (isa(ast,Expr) && ast.head === :incomplete) || break
284 end
285 if !isempty(line)
286 response = eval_with_backend(ast, backend)
287 print_response(repl, response, !ends_with_semicolon(line), false)
288 end
289 write(repl.terminal, '\n')
290 ((!interrupted && isempty(line)) || hit_eof) && break
291 end
292 # terminate backend
293 put!(backend.repl_channel, (nothing, -1))
294 dopushdisplay && popdisplay(d)
295 nothing
296 end
297
298 ## User Options
299
300 mutable struct Options
301 hascolor::Bool
302 extra_keymap::Union{Dict,Vector{<:Dict}}
303 # controls the presumed tab width of code pasted into the REPL.
304 # Must satisfy `0 < tabwidth <= 16`.
305 tabwidth::Int
306 # Maximum number of entries in the kill ring queue.
307 # Beyond this number, oldest entries are discarded first.
308 kill_ring_max::Int
309 region_animation_duration::Float64
310 beep_duration::Float64
311 beep_blink::Float64
312 beep_maxduration::Float64
313 beep_colors::Vector{String}
314 beep_use_current::Bool
315 backspace_align::Bool
316 backspace_adjust::Bool
317 confirm_exit::Bool # ^D must be repeated to confirm exit
318 auto_indent::Bool # indent a newline like line above
319 auto_indent_tmp_off::Bool # switch auto_indent temporarily off if copy&paste
320 auto_indent_bracketed_paste::Bool # set to true if terminal knows paste mode
321 # cancel auto-indent when next character is entered within this time frame :
322 auto_indent_time_threshold::Float64
323 # default IOContext settings at the REPL
324 iocontext::Dict{Symbol,Any}
325 end
326
327 Options(;
328 hascolor = true,
329 extra_keymap = AnyDict[],
330 tabwidth = 8,
331 kill_ring_max = 100,
332 region_animation_duration = 0.2,
333 beep_duration = 0.2, beep_blink = 0.2, beep_maxduration = 1.0,
334 beep_colors = ["\e[90m"], # gray (text_colors not yet available)
335 beep_use_current = true,
336 backspace_align = true, backspace_adjust = backspace_align,
337 confirm_exit = false,
338 auto_indent = true,
339 auto_indent_tmp_off = false,
340 auto_indent_bracketed_paste = false,
341 auto_indent_time_threshold = 0.005,
342 iocontext = Dict{Symbol,Any}()) =
343 Options(hascolor, extra_keymap, tabwidth,
344 kill_ring_max, region_animation_duration,
345 beep_duration, beep_blink, beep_maxduration,
346 beep_colors, beep_use_current,
347 backspace_align, backspace_adjust, confirm_exit,
348 auto_indent, auto_indent_tmp_off, auto_indent_bracketed_paste,
349 auto_indent_time_threshold,
350 iocontext)
351
352 # for use by REPLs not having an options field
353 const GlobalOptions = Options()
354
355
356 ## LineEditREPL ##
357
358 mutable struct LineEditREPL <: AbstractREPL
359 t::TextTerminal
360 hascolor::Bool
361 prompt_color::String
362 input_color::String
363 answer_color::String
364 shell_color::String
365 help_color::String
366 history_file::Bool
367 in_shell::Bool
368 in_help::Bool
369 envcolors::Bool
370 waserror::Bool
371 specialdisplay::Union{Nothing,AbstractDisplay}
372 options::Options
373 mistate::Union{MIState,Nothing}
374 interface::ModalInterface
375 backendref::REPLBackendRef
376 LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,in_help,envcolors) =
377 new(t,true,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,
378 in_help,envcolors,false,nothing, Options(), nothing)
379 end
380 outstream(r::LineEditREPL) = r.t
381 specialdisplay(r::LineEditREPL) = r.specialdisplay
382 specialdisplay(r::AbstractREPL) = nothing
383 terminal(r::LineEditREPL) = r.t
384
385 LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
386 LineEditREPL(t, hascolor,
387 hascolor ? Base.text_colors[:green] : "",
388 hascolor ? Base.input_color() : "",
389 hascolor ? Base.answer_color() : "",
390 hascolor ? Base.text_colors[:red] : "",
391 hascolor ? Base.text_colors[:yellow] : "",
392 false, false, false, envcolors
393 )
394
395 mutable struct REPLCompletionProvider <: CompletionProvider end
396 mutable struct ShellCompletionProvider <: CompletionProvider end
397 struct LatexCompletions <: CompletionProvider end
398
399 beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
400
401 function complete_line(c::REPLCompletionProvider, s)
402 partial = beforecursor(s.input_buffer)
403 full = LineEdit.input_string(s)
404 ret, range, should_complete = completions(full, lastindex(partial))
405 return unique!(map(completion_text, ret)), partial[range], should_complete
406 end
407
408 function complete_line(c::ShellCompletionProvider, s)
409 # First parse everything up to the current position
410 partial = beforecursor(s.input_buffer)
411 full = LineEdit.input_string(s)
412 ret, range, should_complete = shell_completions(full, lastindex(partial))
413 return unique!(map(completion_text, ret)), partial[range], should_complete
414 end
415
416 function complete_line(c::LatexCompletions, s)
417 partial = beforecursor(LineEdit.buffer(s))
418 full = LineEdit.input_string(s)
419 ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
420 return unique!(map(completion_text, ret)), partial[range], should_complete
421 end
422
423 mutable struct REPLHistoryProvider <: HistoryProvider
424 history::Array{String,1}
425 history_file::Union{Nothing,IO}
426 start_idx::Int
427 cur_idx::Int
428 last_idx::Int
429 last_buffer::IOBuffer
430 last_mode::Union{Nothing,Prompt}
431 mode_mapping::Dict
432 modes::Array{Symbol,1}
433 end
434 REPLHistoryProvider(mode_mapping) =
435 REPLHistoryProvider(String[], nothing, 0, 0, -1, IOBuffer(),
436 nothing, mode_mapping, UInt8[])
437
438 invalid_history_message(path::String) = """
439 Invalid history file ($path) format:
440 If you have a history file left over from an older version of Julia,
441 try renaming or deleting it.
442 Invalid character: """
443
444 munged_history_message(path::String) = """
445 Invalid history file ($path) format:
446 An editor may have converted tabs to spaces at line """
447
448 function hist_getline(file)
449 while !eof(file)
450 line = readline(file, keep=true)
451 isempty(line) && return line
452 line[1] in "\r\n" || return line
453 end
454 return ""
455 end
456
457 function hist_from_file(hp, file, path)
458 hp.history_file = file
459 seek(file, 0)
460 countlines = 0
461 while true
462 mode = :julia
463 line = hist_getline(file)
464 isempty(line) && break
465 countlines += 1
466 line[1] != '#' &&
467 error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
468 while !isempty(line)
469 m = match(r"^#\s*(\w+)\s*:\s*(.*?)\s*$", line)
470 m === nothing && break
471 if m.captures[1] == "mode"
472 mode = Symbol(m.captures[2])
473 end
474 line = hist_getline(file)
475 countlines += 1
476 end
477 isempty(line) && break
478 # Make sure starts with tab
479 line[1] == ' ' &&
480 error(munged_history_message(path), countlines)
481 line[1] != '\t' &&
482 error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
483 lines = String[]
484 while !isempty(line)
485 push!(lines, chomp(line[2:end]))
486 eof(file) && break
487 ch = Char(Base.peek(file))
488 ch == ' ' && error(munged_history_message(path), countlines)
489 ch != '\t' && break
490 line = hist_getline(file)
491 countlines += 1
492 end
493 push!(hp.modes, mode)
494 push!(hp.history, join(lines, '\n'))
495 end
496 seekend(file)
497 hp.start_idx = length(hp.history)
498 return hp
499 end
500
501 function mode_idx(hist::REPLHistoryProvider, mode)
502 c = :julia
503 for (k,v) in hist.mode_mapping
504 isequal(v, mode) && (c = k)
505 end
506 return c
507 end
508
509 function add_history(hist::REPLHistoryProvider, s)
510 str = rstrip(String(take!(copy(s.input_buffer))))
511 isempty(strip(str)) && return
512 mode = mode_idx(hist, LineEdit.mode(s))
513 !isempty(hist.history) &&
514 isequal(mode, hist.modes[end]) && str == hist.history[end] && return
515 push!(hist.modes, mode)
516 push!(hist.history, str)
517 hist.history_file === nothing && return
518 entry = """
519 # time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
520 # mode: $mode
521 $(replace(str, r"^"ms => "\t"))
522 """
523 # TODO: write-lock history file
524 seekend(hist.history_file)
525 print(hist.history_file, entry)
526 flush(hist.history_file)
527 nothing
528 end
529
530 function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
531 max_idx = length(hist.history) + 1
532 @assert 1 <= hist.cur_idx <= max_idx
533 (1 <= idx <= max_idx) || return :none
534 idx != hist.cur_idx || return :none
535
536 # save the current line
537 if save_idx == max_idx
538 hist.last_mode = LineEdit.mode(s)
539 hist.last_buffer = copy(LineEdit.buffer(s))
540 else
541 hist.history[save_idx] = LineEdit.input_string(s)
542 hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
543 end
544
545 # load the saved line
546 if idx == max_idx
547 last_buffer = hist.last_buffer
548 LineEdit.transition(s, hist.last_mode) do
549 LineEdit.replace_line(s, last_buffer)
550 end
551 hist.last_mode = nothing
552 hist.last_buffer = IOBuffer()
553 else
554 if haskey(hist.mode_mapping, hist.modes[idx])
555 LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
556 LineEdit.replace_line(s, hist.history[idx])
557 end
558 else
559 return :skip
560 end
561 end
562 hist.cur_idx = idx
563
564 return :ok
565 end
566
567 # REPL History can also transitions modes
568 function LineEdit.accept_result_newmode(hist::REPLHistoryProvider)
569 if 1 <= hist.cur_idx <= length(hist.modes)
570 return hist.mode_mapping[hist.modes[hist.cur_idx]]
571 end
572 return nothing
573 end
574
575 function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
576 num::Int=1, save_idx::Int = hist.cur_idx)
577 num <= 0 && return history_next(s, hist, -num, save_idx)
578 hist.last_idx = -1
579 m = history_move(s, hist, hist.cur_idx-num, save_idx)
580 if m === :ok
581 LineEdit.move_input_start(s)
582 LineEdit.reset_key_repeats(s) do
583 LineEdit.move_line_end(s)
584 end
585 return LineEdit.refresh_line(s)
586 elseif m === :skip
587 return history_prev(s, hist, num+1, save_idx)
588 else
589 return Terminals.beep(s)
590 end
591 end
592
593 function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
594 num::Int=1, save_idx::Int = hist.cur_idx)
595 if num == 0
596 Terminals.beep(s)
597 return
598 end
599 num < 0 && return history_prev(s, hist, -num, save_idx)
600 cur_idx = hist.cur_idx
601 max_idx = length(hist.history) + 1
602 if cur_idx == max_idx && 0 < hist.last_idx
603 # issue #6312
604 cur_idx = hist.last_idx
605 hist.last_idx = -1
606 end
607 m = history_move(s, hist, cur_idx+num, save_idx)
608 if m === :ok
609 LineEdit.move_input_end(s)
610 return LineEdit.refresh_line(s)
611 elseif m === :skip
612 return history_next(s, hist, num+1, save_idx)
613 else
614 return Terminals.beep(s)
615 end
616 end
617
618 history_first(s::LineEdit.MIState, hist::REPLHistoryProvider) =
619 history_prev(s, hist, hist.cur_idx - 1 -
620 (hist.cur_idx > hist.start_idx+1 ? hist.start_idx : 0))
621
622 history_last(s::LineEdit.MIState, hist::REPLHistoryProvider) =
623 history_next(s, hist, length(hist.history) - hist.cur_idx + 1)
624
625 function history_move_prefix(s::LineEdit.PrefixSearchState,
626 hist::REPLHistoryProvider,
627 prefix::AbstractString,
628 backwards::Bool,
629 cur_idx = hist.cur_idx)
630 cur_response = String(take!(copy(LineEdit.buffer(s))))
631 # when searching forward, start at last_idx
632 if !backwards && hist.last_idx > 0
633 cur_idx = hist.last_idx
634 end
635 hist.last_idx = -1
636 max_idx = length(hist.history)+1
637 idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):max_idx)
638 for idx in idxs
639 if (idx == max_idx) || (startswith(hist.history[idx], prefix) && (hist.history[idx] != cur_response || hist.modes[idx] != LineEdit.mode(s)))
640 m = history_move(s, hist, idx)
641 if m === :ok
642 if idx == max_idx
643 # on resuming the in-progress edit, leave the cursor where the user last had it
644 elseif isempty(prefix)
645 # on empty prefix search, move cursor to the end
646 LineEdit.move_input_end(s)
647 else
648 # otherwise, keep cursor at the prefix position as a visual cue
649 seek(LineEdit.buffer(s), sizeof(prefix))
650 end
651 LineEdit.refresh_line(s)
652 return :ok
653 elseif m === :skip
654 return history_move_prefix(s,hist,prefix,backwards,idx)
655 end
656 end
657 end
658 Terminals.beep(s)
659 nothing
660 end
661 history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
662 history_move_prefix(s, hist, prefix, false)
663 history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
664 history_move_prefix(s, hist, prefix, true)
665
666 function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
667 backwards::Bool=false, skip_current::Bool=false)
668
669 qpos = position(query_buffer)
670 qpos > 0 || return true
671 searchdata = beforecursor(query_buffer)
672 response_str = String(take!(copy(response_buffer)))
673
674 # Alright, first try to see if the current match still works
675 a = position(response_buffer) + 1 # position is zero-indexed
676 # FIXME: I'm pretty sure this is broken since it uses an index
677 # into the search data to index into the response string
678 b = a + sizeof(searchdata)
679 b = b ≤ ncodeunits(response_str) ? prevind(response_str, b) : b-1
680 b = min(lastindex(response_str), b) # ensure that b is valid
681
682 searchfunc1, searchfunc2, searchstart, skipfunc = backwards ?
683 (findlast, findprev, b, prevind) :
684 (findfirst, findnext, a, nextind)
685
686 if searchdata == response_str[a:b]
687 if skip_current
688 searchstart = skipfunc(response_str, searchstart)
689 else
690 return true
691 end
692 end
693
694 # Start searching
695 # First the current response buffer
696 if 1 <= searchstart <= lastindex(response_str)
697 match = searchfunc2(searchdata, response_str, searchstart)
698 if match !== nothing
699 seek(response_buffer, first(match) - 1)
700 return true
701 end
702 end
703
704 # Now search all the other buffers
705 idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):length(hist.history))
706 for idx in idxs
707 h = hist.history[idx]
708 match = searchfunc1(searchdata, h)
709 if match !== nothing && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
710 truncate(response_buffer, 0)
711 write(response_buffer, h)
712 seek(response_buffer, first(match) - 1)
713 hist.cur_idx = idx
714 return true
715 end
716 end
717
718 return false
719 end
720
721 function history_reset_state(hist::REPLHistoryProvider)
722 if hist.cur_idx != length(hist.history) + 1
723 hist.last_idx = hist.cur_idx
724 hist.cur_idx = length(hist.history) + 1
725 end
726 nothing
727 end
728 LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
729
730 function return_callback(s)
731 ast = Base.parse_input_line(String(take!(copy(LineEdit.buffer(s)))), depwarn=false)
732 return !(isa(ast, Expr) && ast.head === :incomplete)
733 end
734
735 find_hist_file() = get(ENV, "JULIA_HISTORY",
736 !isempty(DEPOT_PATH) ? joinpath(DEPOT_PATH[1], "logs", "repl_history.jl") :
737 error("DEPOT_PATH is empty and and ENV[\"JULIA_HISTORY\"] not set."))
738
739 backend(r::AbstractREPL) = r.backendref
740
741 function eval_with_backend(ast, backend::REPLBackendRef)
742 put!(backend.repl_channel, (ast, 1))
743 take!(backend.response_channel) # (val, iserr)
744 end
745
746 function respond(f, repl, main; pass_empty = false, suppress_on_semicolon = true)
747 return function do_respond(s, buf, ok)
748 if !ok
749 return transition(s, :abort)
750 end
751 line = String(take!(buf))
752 if !isempty(line) || pass_empty
753 reset(repl)
754 local response
755 try
756 ast = Base.invokelatest(f, line)
757 response = eval_with_backend(ast, backend(repl))
758 catch
759 response = (catch_stack(), true)
760 end
761 hide_output = suppress_on_semicolon && ends_with_semicolon(line)
762 print_response(repl, response, !hide_output, Base.have_color)
763 end
764 prepare_next(repl)
765 reset_state(s)
766 return s.current_mode.sticky ? true : transition(s, main)
767 end
768 end
769
770 function reset(repl::LineEditREPL)
771 raw!(repl.t, false)
772 print(repl.t, Base.text_colors[:normal])
773 end
774
775 function prepare_next(repl::LineEditREPL)
776 println(terminal(repl))
777 end
778
779 function mode_keymap(julia_prompt::Prompt)
780 AnyDict(
781 '\b' => function (s,o...)
782 if isempty(s) || position(LineEdit.buffer(s)) == 0
783 buf = copy(LineEdit.buffer(s))
784 transition(s, julia_prompt) do
785 LineEdit.state(s, julia_prompt).input_buffer = buf
786 end
787 else
788 LineEdit.edit_backspace(s)
789 end
790 end,
791 "^C" => function (s,o...)
792 LineEdit.move_input_end(s)
793 LineEdit.refresh_line(s)
794 print(LineEdit.terminal(s), "^C\n\n")
795 transition(s, julia_prompt)
796 transition(s, :reset)
797 LineEdit.refresh_line(s)
798 end)
799 end
800
801 repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(max(length(hp.history)-hp.start_idx, 1))]"
802 repl_filename(repl, hp) = "REPL"
803
804 const JL_PROMPT_PASTE = Ref(true)
805 enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
806
807 setup_interface(
808 repl::LineEditREPL;
809 # those keyword arguments may be deprecated eventually in favor of the Options mechanism
810 hascolor::Bool = repl.options.hascolor,
811 extra_repl_keymap::Any = repl.options.extra_keymap
812 ) = setup_interface(repl, hascolor, extra_repl_keymap)
813
814 # This non keyword method can be precompiled which is important
815 function setup_interface(
816 repl::LineEditREPL,
817 hascolor::Bool,
818 extra_repl_keymap::Any, # Union{Dict,Vector{<:Dict}},
819 )
820 # The precompile statement emitter has problem outputting valid syntax for the
821 # type of `Union{Dict,Vector{<:Dict}}` (see #28808).
822 # This function is however important to precompile for REPL startup time, therefore,
823 # make the type Any and just assert that we have the correct type below.
824 @assert extra_repl_keymap isa Union{Dict,Vector{<:Dict}}
825
826 ###
827 #
828 # This function returns the main interface that describes the REPL
829 # functionality, it is called internally by functions that setup a
830 # Terminal-based REPL frontend, but if you want to customize your REPL
831 # or embed the REPL in another interface, you may call this function
832 # directly and append it to your interface.
833 #
834 # Usage:
835 #
836 # repl_channel,response_channel = Channel(),Channel()
837 # start_repl_backend(repl_channel, response_channel)
838 # setup_interface(REPLDisplay(t),repl_channel,response_channel)
839 #
840 ###
841
842 ###
843 # We setup the interface in two stages.
844 # First, we set up all components (prompt,rsearch,shell,help)
845 # Second, we create keymaps with appropriate transitions between them
846 # and assign them to the components
847 #
848 ###
849
850 ############################### Stage I ################################
851
852 # This will provide completions for REPL and help mode
853 replc = REPLCompletionProvider()
854
855 # Set up the main Julia prompt
856 julia_prompt = Prompt(JULIA_PROMPT;
857 # Copy colors from the prompt object
858 prompt_prefix = hascolor ? repl.prompt_color : "",
859 prompt_suffix = hascolor ?
860 (repl.envcolors ? Base.input_color : repl.input_color) : "",
861 repl = repl,
862 complete = replc,
863 on_enter = return_callback)
864
865 # Setup help mode
866 help_mode = Prompt("help?> ",
867 prompt_prefix = hascolor ? repl.help_color : "",
868 prompt_suffix = hascolor ?
869 (repl.envcolors ? Base.input_color : repl.input_color) : "",
870 repl = repl,
871 complete = replc,
872 # When we're done transform the entered line into a call to help("$line")
873 on_done = respond(helpmode, repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))
874
875 # Set up shell mode
876 shell_mode = Prompt("shell> ";
877 prompt_prefix = hascolor ? repl.shell_color : "",
878 prompt_suffix = hascolor ?
879 (repl.envcolors ? Base.input_color : repl.input_color) : "",
880 repl = repl,
881 complete = ShellCompletionProvider(),
882 # Transform "foo bar baz" into `foo bar baz` (shell quoting)
883 # and pass into Base.repl_cmd for processing (handles `ls` and `cd`
884 # special)
885 on_done = respond(repl, julia_prompt) do line
886 Expr(:call, :(Base.repl_cmd),
887 :(Base.cmd_gen($(Base.shell_parse(line)[1]))),
888 outstream(repl))
889 end)
890
891
892 ################################# Stage II #############################
893
894 # Setup history
895 # We will have a unified history for all REPL modes
896 hp = REPLHistoryProvider(Dict{Symbol,Any}(:julia => julia_prompt,
897 :shell => shell_mode,
898 :help => help_mode))
899 if repl.history_file
900 try
901 hist_path = find_hist_file()
902 mkpath(dirname(hist_path))
903 f = open(hist_path, read=true, write=true, create=true)
904 finalizer(replc) do replc
905 close(f)
906 end
907 hist_from_file(hp, f, hist_path)
908 catch
909 print_response(repl, (catch_stack(),true), true, Base.have_color)
910 println(outstream(repl))
911 @info "Disabling history file for this session"
912 repl.history_file = false
913 end
914 end
915 history_reset_state(hp)
916 julia_prompt.hist = hp
917 shell_mode.hist = hp
918 help_mode.hist = hp
919
920 julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
921
922
923 search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
924 search_prompt.complete = LatexCompletions()
925
926 # Canonicalize user keymap input
927 if isa(extra_repl_keymap, Dict)
928 extra_repl_keymap = [extra_repl_keymap]
929 end
930
931 repl_keymap = AnyDict(
932 ';' => function (s,o...)
933 if isempty(s) || position(LineEdit.buffer(s)) == 0
934 buf = copy(LineEdit.buffer(s))
935 transition(s, shell_mode) do
936 LineEdit.state(s, shell_mode).input_buffer = buf
937 end
938 else
939 edit_insert(s, ';')
940 end
941 end,
942 '?' => function (s,o...)
943 if isempty(s) || position(LineEdit.buffer(s)) == 0
944 buf = copy(LineEdit.buffer(s))
945 transition(s, help_mode) do
946 LineEdit.state(s, help_mode).input_buffer = buf
947 end
948 else
949 edit_insert(s, '?')
950 end
951 end,
952
953 # Bracketed Paste Mode
954 "\e[200~" => (s,o...)->begin
955 input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
956 sbuffer = LineEdit.buffer(s)
957 curspos = position(sbuffer)
958 seek(sbuffer, 0)
959 shouldeval = (bytesavailable(sbuffer) == curspos && !occursin(UInt8('\n'), sbuffer))
960 seek(sbuffer, curspos)
961 if curspos == 0
962 # if pasting at the beginning, strip leading whitespace
963 input = lstrip(input)
964 end
965 if !shouldeval
966 # when pasting in the middle of input, just paste in place
967 # don't try to execute all the WIP, since that's rather confusing
968 # and is often ill-defined how it should behave
969 edit_insert(s, input)
970 return
971 end
972 LineEdit.push_undo(s)
973 edit_insert(sbuffer, input)
974 input = String(take!(sbuffer))
975 oldpos = firstindex(input)
976 firstline = true
977 isprompt_paste = false
978 jl_prompt_len = 7 # "julia> "
979 while oldpos <= lastindex(input) # loop until all lines have been executed
980 if JL_PROMPT_PASTE[]
981 # Check if the next statement starts with "julia> ", in that case
982 # skip it. But first skip whitespace
983 while input[oldpos] in ('\n', ' ', '\t')
984 oldpos = nextind(input, oldpos)
985 oldpos >= sizeof(input) && return
986 end
987 # Check if input line starts with "julia> ", remove it if we are in prompt paste mode
988 if (firstline || isprompt_paste) && startswith(SubString(input, oldpos), JULIA_PROMPT)
989 isprompt_paste = true
990 oldpos += jl_prompt_len
991 # If we are prompt pasting and current statement does not begin with julia> , skip to next line
992 elseif isprompt_paste
993 while input[oldpos] != '\n'
994 oldpos = nextind(input, oldpos)
995 oldpos >= sizeof(input) && return
996 end
997 continue
998 end
999 end
1000 ast, pos = Meta.parse(input, oldpos, raise=false, depwarn=false)
1001 if (isa(ast, Expr) && (ast.head === :error || ast.head === :incomplete)) ||
1002 (pos > ncodeunits(input) && !endswith(input, '\n'))
1003 # remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
1004 # Insert all the remaining text as one line (might be empty)
1005 tail = input[oldpos:end]
1006 if !firstline
1007 # strip leading whitespace, but only if it was the result of executing something
1008 # (avoids modifying the user's current leading wip line)
1009 tail = lstrip(tail)
1010 end
1011 if isprompt_paste # remove indentation spaces corresponding to the prompt
1012 tail = replace(tail, r"^"m * ' '^jl_prompt_len => "")
1013 end
1014 LineEdit.replace_line(s, tail, true)
1015 LineEdit.refresh_line(s)
1016 break
1017 end
1018 # get the line and strip leading and trailing whitespace
1019 line = strip(input[oldpos:prevind(input, pos)])
1020 if !isempty(line)
1021 if isprompt_paste # remove indentation spaces corresponding to the prompt
1022 line = replace(line, r"^"m * ' '^jl_prompt_len => "")
1023 end
1024 # put the line on the screen and history
1025 LineEdit.replace_line(s, line)
1026 LineEdit.commit_line(s)
1027 # execute the statement
1028 terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
1029 raw!(terminal, false) && disable_bracketed_paste(terminal)
1030 LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
1031 raw!(terminal, true) && enable_bracketed_paste(terminal)
1032 LineEdit.push_undo(s) # when the last line is incomplete
1033 end
1034 oldpos = pos
1035 firstline = false
1036 end
1037 end,
1038
1039 # Open the editor at the location of a stackframe or method
1040 # This is accessing a global variable that gets set in
1041 # the show_backtrace and show_method_table functions.
1042 "^Q" => (s, o...) -> begin
1043 linfos = Base.LAST_SHOWN_LINE_INFOS
1044 str = String(take!(LineEdit.buffer(s)))
1045 n = tryparse(Int, str)
1046 n === nothing && @goto writeback
1047 if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "./REPL")
1048 @goto writeback
1049 end
1050 InteractiveUtils.edit(linfos[n][1], linfos[n][2])
1051 LineEdit.refresh_line(s)
1052 return
1053 @label writeback
1054 write(LineEdit.buffer(s), str)
1055 return
1056 end,
1057 )
1058
1059 prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
1060
1061 a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1062 prepend!(a, extra_repl_keymap)
1063
1064 julia_prompt.keymap_dict = LineEdit.keymap(a)
1065
1066 mk = mode_keymap(julia_prompt)
1067
1068 b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
1069 prepend!(b, extra_repl_keymap)
1070
1071 shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
1072
1073 allprompts = [julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt]
1074 return ModalInterface(allprompts)
1075 end
1076
1077 function run_frontend(repl::LineEditREPL, backend::REPLBackendRef)
1078 d = REPLDisplay(repl)
1079 dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
1080 dopushdisplay && pushdisplay(d)
1081 if !isdefined(repl,:interface)
1082 interface = repl.interface = setup_interface(repl)
1083 else
1084 interface = repl.interface
1085 end
1086 repl.backendref = backend
1087 repl.mistate = LineEdit.init_state(terminal(repl), interface)
1088 run_interface(terminal(repl), interface, repl.mistate)
1089 dopushdisplay && popdisplay(d)
1090 nothing
1091 end
1092
1093 ## StreamREPL ##
1094
1095 mutable struct StreamREPL <: AbstractREPL
1096 stream::IO
1097 prompt_color::String
1098 input_color::String
1099 answer_color::String
1100 waserror::Bool
1101 StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
1102 end
1103 StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
1104 run_repl(stream::IO) = run_repl(StreamREPL(stream))
1105
1106 outstream(s::StreamREPL) = s.stream
1107
1108 answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
1109 answer_color(r::StreamREPL) = r.answer_color
1110 input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
1111 input_color(r::StreamREPL) = r.input_color
1112
1113 # heuristic function to decide if the presence of a semicolon
1114 # at the end of the expression was intended for suppressing output
1115 function ends_with_semicolon(line::AbstractString)
1116 match = findlast(isequal(';'), line)
1117 if match !== nothing
1118 # state for comment parser, assuming that the `;` isn't in a string or comment
1119 # so input like ";#" will still thwart this to give the wrong (anti-conservative) answer
1120 comment = false
1121 comment_start = false
1122 comment_close = false
1123 comment_multi = 0
1124 for c in line[(match + 1):end]
1125 if comment_multi > 0
1126 # handle nested multi-line comments
1127 if comment_close && c == '#'
1128 comment_close = false
1129 comment_multi -= 1
1130 elseif comment_start && c == '='
1131 comment_start = false
1132 comment_multi += 1
1133 else
1134 comment_start = (c == '#')
1135 comment_close = (c == '=')
1136 end
1137 elseif comment
1138 # handle line comments
1139 if c == '\r' || c == '\n'
1140 comment = false
1141 end
1142 elseif comment_start
1143 # see what kind of comment this is
1144 comment_start = false
1145 if c == '='
1146 comment_multi = 1
1147 else
1148 comment = true
1149 end
1150 elseif c == '#'
1151 # start handling for a comment
1152 comment_start = true
1153 else
1154 # outside of a comment, encountering anything but whitespace
1155 # means the semi-colon was internal to the expression
1156 isspace(c) || return false
1157 end
1158 end
1159 return true
1160 end
1161 return false
1162 end
1163
1164 function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
1165 have_color = Base.have_color
1166 Base.banner(repl.stream)
1167 d = REPLDisplay(repl)
1168 dopushdisplay = !in(d,Base.Multimedia.displays)
1169 dopushdisplay && pushdisplay(d)
1170 while !eof(repl.stream)
1171 if have_color
1172 print(repl.stream,repl.prompt_color)
1173 end
1174 print(repl.stream, "julia> ")
1175 if have_color
1176 print(repl.stream, input_color(repl))
1177 end
1178 line = readline(repl.stream, keep=true)
1179 if !isempty(line)
1180 ast = Base.parse_input_line(line)
1181 if have_color
1182 print(repl.stream, Base.color_normal)
1183 end
1184 response = eval_with_backend(ast, backend)
1185 print_response(repl, response, !ends_with_semicolon(line), have_color)
1186 end
1187 end
1188 # Terminate Backend
1189 put!(backend.repl_channel, (nothing, -1))
1190 dopushdisplay && popdisplay(d)
1191 nothing
1192 end
1193
1194 function start_repl_server(port::Int)
1195 return listen(port) do server, status
1196 client = accept(server)
1197 run_repl(client)
1198 nothing
1199 end
1200 end
1201
1202 end # module