| 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
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
58 (100 %) (incl.) when called from repl_backend_loop line 246 |
||
| 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
function repl_backend_loop(backend::REPLBackend, get_module::Function)
58 (100 %) (incl.) when called from #start_repl_backend#46 line 231 |
||
| 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 |