| 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
function eval_user_input(@nospecialize(ast), backend::REPLBackend)
833 (100 %) (incl.) when called from run_backend line 1070 |
||
| 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 |