1068 lines
36 KiB
Julia
1068 lines
36 KiB
Julia
# This file is a part of Julia. License is MIT: https://julialang.org/license
|
|
|
|
module REPL
|
|
|
|
using Base.Meta
|
|
using ..Terminals
|
|
using ..LineEdit
|
|
using ..REPLCompletions
|
|
|
|
export
|
|
BasicREPL,
|
|
LineEditREPL,
|
|
StreamREPL
|
|
|
|
import Base:
|
|
Display,
|
|
display,
|
|
show,
|
|
AnyDict,
|
|
==
|
|
|
|
import ..LineEdit:
|
|
CompletionProvider,
|
|
HistoryProvider,
|
|
add_history,
|
|
complete_line,
|
|
history_next,
|
|
history_next_prefix,
|
|
history_prev,
|
|
history_prev_prefix,
|
|
history_search,
|
|
accept_result,
|
|
terminal
|
|
|
|
abstract type AbstractREPL end
|
|
|
|
answer_color(::AbstractREPL) = ""
|
|
|
|
const JULIA_PROMPT = "julia> "
|
|
|
|
mutable struct REPLBackend
|
|
"channel for AST"
|
|
repl_channel::Channel
|
|
"channel for results: (value, nothing) or (error, backtrace)"
|
|
response_channel::Channel
|
|
"flag indicating the state of this backend"
|
|
in_eval::Bool
|
|
"current backend task"
|
|
backend_task::Task
|
|
|
|
REPLBackend(repl_channel, response_channel, in_eval) =
|
|
new(repl_channel, response_channel, in_eval)
|
|
end
|
|
|
|
function eval_user_input(ast::ANY, backend::REPLBackend)
|
|
iserr, lasterr = false, ((), nothing)
|
|
Base.sigatomic_begin()
|
|
while true
|
|
try
|
|
Base.sigatomic_end()
|
|
if iserr
|
|
put!(backend.response_channel, lasterr)
|
|
iserr, lasterr = false, ()
|
|
else
|
|
backend.in_eval = true
|
|
value = eval(Main, ast)
|
|
backend.in_eval = false
|
|
# note: value wrapped carefully here to ensure it doesn't get passed through expand
|
|
eval(Main, Expr(:body, Expr(:(=), :ans, QuoteNode(value)), Expr(:return, nothing)))
|
|
put!(backend.response_channel, (value, nothing))
|
|
end
|
|
break
|
|
catch err
|
|
if iserr
|
|
println("SYSTEM ERROR: Failed to report error to REPL frontend")
|
|
println(err)
|
|
end
|
|
iserr, lasterr = true, (err, catch_backtrace())
|
|
end
|
|
end
|
|
Base.sigatomic_end()
|
|
end
|
|
|
|
function start_repl_backend(repl_channel::Channel, response_channel::Channel)
|
|
backend = REPLBackend(repl_channel, response_channel, false)
|
|
backend.backend_task = @schedule begin
|
|
# include looks at this to determine the relative include path
|
|
# nothing means cwd
|
|
while true
|
|
tls = task_local_storage()
|
|
tls[:SOURCE_PATH] = nothing
|
|
ast, show_value = take!(backend.repl_channel)
|
|
if show_value == -1
|
|
# exit flag
|
|
break
|
|
end
|
|
eval_user_input(ast, backend)
|
|
end
|
|
end
|
|
backend
|
|
end
|
|
|
|
function ip_matches_func(ip, func::Symbol)
|
|
for fr in StackTraces.lookup(ip)
|
|
if fr === StackTraces.UNKNOWN || fr.from_c
|
|
return false
|
|
end
|
|
fr.func === func && return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
struct REPLDisplay{R<:AbstractREPL} <: Display
|
|
repl::R
|
|
end
|
|
|
|
==(a::REPLDisplay, b::REPLDisplay) = a.repl === b.repl
|
|
|
|
function display(d::REPLDisplay, mime::MIME"text/plain", x)
|
|
io = outstream(d.repl)
|
|
Base.have_color && write(io, answer_color(d.repl))
|
|
show(IOContext(io, :limit => true), mime, x)
|
|
println(io)
|
|
end
|
|
display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
|
|
|
|
function print_response(repl::AbstractREPL, val::ANY, bt, show_value::Bool, have_color::Bool)
|
|
repl.waserror = bt !== nothing
|
|
print_response(outstream(repl), val, bt, show_value, have_color, specialdisplay(repl))
|
|
end
|
|
function print_response(errio::IO, val::ANY, bt, show_value::Bool, have_color::Bool, specialdisplay=nothing)
|
|
Base.sigatomic_begin()
|
|
while true
|
|
try
|
|
Base.sigatomic_end()
|
|
if bt !== nothing
|
|
eval(Main, Expr(:body, Expr(:return, Expr(:call, Base.display_error,
|
|
errio, QuoteNode(val), bt))))
|
|
iserr, lasterr = false, ()
|
|
else
|
|
if val !== nothing && show_value
|
|
try
|
|
if specialdisplay === nothing
|
|
eval(Main, Expr(:body, Expr(:return, Expr(:call, display, QuoteNode(val)))))
|
|
else
|
|
eval(Main, Expr(:body, Expr(:return, Expr(:call, specialdisplay, QuoteNode(val)))))
|
|
end
|
|
catch err
|
|
println(errio, "Error showing value of type ", typeof(val), ":")
|
|
rethrow(err)
|
|
end
|
|
end
|
|
end
|
|
break
|
|
catch err
|
|
if bt !== nothing
|
|
println(errio, "SYSTEM: show(lasterr) caused an error")
|
|
println(errio, err)
|
|
Base.show_backtrace(errio, bt)
|
|
break
|
|
end
|
|
val = err
|
|
bt = catch_backtrace()
|
|
end
|
|
end
|
|
Base.sigatomic_end()
|
|
end
|
|
|
|
# A reference to a backend
|
|
struct REPLBackendRef
|
|
repl_channel::Channel
|
|
response_channel::Channel
|
|
end
|
|
|
|
function run_repl(repl::AbstractREPL, consumer = x->nothing)
|
|
repl_channel = Channel(1)
|
|
response_channel = Channel(1)
|
|
backend = start_repl_backend(repl_channel, response_channel)
|
|
consumer(backend)
|
|
run_frontend(repl, REPLBackendRef(repl_channel,response_channel))
|
|
return backend
|
|
end
|
|
|
|
## BasicREPL ##
|
|
|
|
mutable struct BasicREPL <: AbstractREPL
|
|
terminal::TextTerminal
|
|
waserror::Bool
|
|
BasicREPL(t) = new(t,false)
|
|
end
|
|
|
|
outstream(r::BasicREPL) = r.terminal
|
|
|
|
function run_frontend(repl::BasicREPL, backend::REPLBackendRef)
|
|
d = REPLDisplay(repl)
|
|
dopushdisplay = !in(d,Base.Multimedia.displays)
|
|
dopushdisplay && pushdisplay(d)
|
|
repl_channel, response_channel = backend.repl_channel, backend.response_channel
|
|
hit_eof = false
|
|
while true
|
|
Base.reseteof(repl.terminal)
|
|
write(repl.terminal, JULIA_PROMPT)
|
|
line = ""
|
|
ast = nothing
|
|
interrupted = false
|
|
while true
|
|
try
|
|
line *= readline(repl.terminal, chomp=false)
|
|
catch e
|
|
if isa(e,InterruptException)
|
|
try # raise the debugger if present
|
|
ccall(:jl_raise_debugger, Int, ())
|
|
end
|
|
line = ""
|
|
interrupted = true
|
|
break
|
|
elseif isa(e,EOFError)
|
|
hit_eof = true
|
|
break
|
|
else
|
|
rethrow()
|
|
end
|
|
end
|
|
ast = Base.parse_input_line(line)
|
|
(isa(ast,Expr) && ast.head == :incomplete) || break
|
|
end
|
|
if !isempty(line)
|
|
put!(repl_channel, (ast, 1))
|
|
val, bt = take!(response_channel)
|
|
if !ends_with_semicolon(line)
|
|
print_response(repl, val, bt, true, false)
|
|
end
|
|
end
|
|
write(repl.terminal, '\n')
|
|
((!interrupted && isempty(line)) || hit_eof) && break
|
|
end
|
|
# terminate backend
|
|
put!(repl_channel, (nothing, -1))
|
|
dopushdisplay && popdisplay(d)
|
|
end
|
|
|
|
## LineEditREPL ##
|
|
|
|
mutable struct LineEditREPL <: AbstractREPL
|
|
t::TextTerminal
|
|
hascolor::Bool
|
|
prompt_color::String
|
|
input_color::String
|
|
answer_color::String
|
|
shell_color::String
|
|
help_color::String
|
|
history_file::Bool
|
|
in_shell::Bool
|
|
in_help::Bool
|
|
envcolors::Bool
|
|
waserror::Bool
|
|
specialdisplay
|
|
interface
|
|
backendref::REPLBackendRef
|
|
LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,in_help,envcolors) =
|
|
new(t,true,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,
|
|
in_help,envcolors,false,nothing)
|
|
end
|
|
outstream(r::LineEditREPL) = r.t
|
|
specialdisplay(r::LineEditREPL) = r.specialdisplay
|
|
specialdisplay(r::AbstractREPL) = nothing
|
|
terminal(r::LineEditREPL) = r.t
|
|
|
|
LineEditREPL(t::TextTerminal, envcolors = false) = LineEditREPL(t,
|
|
true,
|
|
Base.text_colors[:green],
|
|
Base.input_color(),
|
|
Base.answer_color(),
|
|
Base.text_colors[:red],
|
|
Base.text_colors[:yellow],
|
|
false, false, false, envcolors)
|
|
|
|
mutable struct REPLCompletionProvider <: CompletionProvider; end
|
|
|
|
mutable struct ShellCompletionProvider <: CompletionProvider; end
|
|
|
|
struct LatexCompletions <: CompletionProvider; end
|
|
|
|
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
|
|
|
|
function complete_line(c::REPLCompletionProvider, s)
|
|
partial = beforecursor(s.input_buffer)
|
|
full = LineEdit.input_string(s)
|
|
ret, range, should_complete = completions(full, endof(partial))
|
|
return ret, partial[range], should_complete
|
|
end
|
|
|
|
function complete_line(c::ShellCompletionProvider, s)
|
|
# First parse everything up to the current position
|
|
partial = beforecursor(s.input_buffer)
|
|
full = LineEdit.input_string(s)
|
|
ret, range, should_complete = shell_completions(full, endof(partial))
|
|
return ret, partial[range], should_complete
|
|
end
|
|
|
|
function complete_line(c::LatexCompletions, s)
|
|
partial = beforecursor(LineEdit.buffer(s))
|
|
full = LineEdit.input_string(s)
|
|
ret, range, should_complete = bslash_completions(full, endof(partial))[2]
|
|
return ret, partial[range], should_complete
|
|
end
|
|
|
|
|
|
mutable struct REPLHistoryProvider <: HistoryProvider
|
|
history::Array{String,1}
|
|
history_file
|
|
start_idx::Int
|
|
cur_idx::Int
|
|
last_idx::Int
|
|
last_buffer::IOBuffer
|
|
last_mode
|
|
mode_mapping
|
|
modes::Array{Symbol,1}
|
|
end
|
|
REPLHistoryProvider(mode_mapping) =
|
|
REPLHistoryProvider(String[], nothing, 0, 0, -1, IOBuffer(),
|
|
nothing, mode_mapping, UInt8[])
|
|
|
|
invalid_history_message(path::String) = """
|
|
Invalid history file ($path) format:
|
|
If you have a history file left over from an older version of Julia,
|
|
try renaming or deleting it.
|
|
Invalid character: """
|
|
|
|
munged_history_message(path::String) = """
|
|
Invalid history file ($path) format:
|
|
An editor may have converted tabs to spaces at line """
|
|
|
|
function hist_getline(file)
|
|
while !eof(file)
|
|
line = readline(file, chomp=false)
|
|
isempty(line) && return line
|
|
line[1] in "\r\n" || return line
|
|
end
|
|
return ""
|
|
end
|
|
|
|
function hist_from_file(hp, file, path)
|
|
hp.history_file = file
|
|
seek(file, 0)
|
|
countlines = 0
|
|
while true
|
|
mode = :julia
|
|
line = hist_getline(file)
|
|
isempty(line) && break
|
|
countlines += 1
|
|
line[1] != '#' &&
|
|
error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
|
|
while !isempty(line)
|
|
m = match(r"^#\s*(\w+)\s*:\s*(.*?)\s*$", line)
|
|
m === nothing && break
|
|
if m.captures[1] == "mode"
|
|
mode = Symbol(m.captures[2])
|
|
end
|
|
line = hist_getline(file)
|
|
countlines += 1
|
|
end
|
|
isempty(line) && break
|
|
# Make sure starts with tab
|
|
line[1] == ' ' &&
|
|
error(munged_history_message(path), countlines)
|
|
line[1] != '\t' &&
|
|
error(invalid_history_message(path), repr(line[1]), " at line ", countlines)
|
|
lines = String[]
|
|
while !isempty(line)
|
|
push!(lines, chomp(line[2:end]))
|
|
eof(file) && break
|
|
ch = Char(Base.peek(file))
|
|
ch == ' ' && error(munged_history_message(path), countlines)
|
|
ch != '\t' && break
|
|
line = hist_getline(file)
|
|
countlines += 1
|
|
end
|
|
push!(hp.modes, mode)
|
|
push!(hp.history, join(lines, '\n'))
|
|
end
|
|
seekend(file)
|
|
hp.start_idx = length(hp.history)
|
|
hp
|
|
end
|
|
|
|
function mode_idx(hist::REPLHistoryProvider, mode)
|
|
c = :julia
|
|
for (k,v) in hist.mode_mapping
|
|
isequal(v, mode) && (c = k)
|
|
end
|
|
return c
|
|
end
|
|
|
|
function add_history(hist::REPLHistoryProvider, s)
|
|
str = rstrip(String(s.input_buffer))
|
|
isempty(strip(str)) && return
|
|
mode = mode_idx(hist, LineEdit.mode(s))
|
|
!isempty(hist.history) &&
|
|
isequal(mode, hist.modes[end]) && str == hist.history[end] && return
|
|
push!(hist.modes, mode)
|
|
push!(hist.history, str)
|
|
hist.history_file === nothing && return
|
|
entry = """
|
|
# time: $(Libc.strftime("%Y-%m-%d %H:%M:%S %Z", time()))
|
|
# mode: $mode
|
|
$(replace(str, r"^"ms, "\t"))
|
|
"""
|
|
# TODO: write-lock history file
|
|
seekend(hist.history_file)
|
|
print(hist.history_file, entry)
|
|
flush(hist.history_file)
|
|
end
|
|
|
|
function history_move(s::Union{LineEdit.MIState,LineEdit.PrefixSearchState}, hist::REPLHistoryProvider, idx::Int, save_idx::Int = hist.cur_idx)
|
|
max_idx = length(hist.history) + 1
|
|
@assert 1 <= hist.cur_idx <= max_idx
|
|
(1 <= idx <= max_idx) || return :none
|
|
idx != hist.cur_idx || return :none
|
|
|
|
# save the current line
|
|
if save_idx == max_idx
|
|
hist.last_mode = LineEdit.mode(s)
|
|
hist.last_buffer = copy(LineEdit.buffer(s))
|
|
else
|
|
hist.history[save_idx] = LineEdit.input_string(s)
|
|
hist.modes[save_idx] = mode_idx(hist, LineEdit.mode(s))
|
|
end
|
|
|
|
# load the saved line
|
|
if idx == max_idx
|
|
last_buffer = hist.last_buffer
|
|
LineEdit.transition(s, hist.last_mode) do
|
|
LineEdit.replace_line(s, last_buffer)
|
|
end
|
|
hist.last_mode = nothing
|
|
hist.last_buffer = IOBuffer()
|
|
else
|
|
if haskey(hist.mode_mapping, hist.modes[idx])
|
|
LineEdit.transition(s, hist.mode_mapping[hist.modes[idx]]) do
|
|
LineEdit.replace_line(s, hist.history[idx])
|
|
end
|
|
else
|
|
return :skip
|
|
end
|
|
end
|
|
hist.cur_idx = idx
|
|
|
|
return :ok
|
|
end
|
|
|
|
# Modified version of accept_result that also transitions modes
|
|
function LineEdit.accept_result(s, p::LineEdit.HistoryPrompt{REPLHistoryProvider})
|
|
parent = LineEdit.state(s, p).parent
|
|
hist = p.hp
|
|
if 1 <= hist.cur_idx <= length(hist.modes)
|
|
m = hist.mode_mapping[hist.modes[hist.cur_idx]]
|
|
LineEdit.transition(s, m) do
|
|
LineEdit.replace_line(LineEdit.state(s, m), LineEdit.state(s, p).response_buffer)
|
|
end
|
|
else
|
|
LineEdit.transition(s, parent)
|
|
end
|
|
end
|
|
|
|
function history_prev(s::LineEdit.MIState, hist::REPLHistoryProvider,
|
|
save_idx::Int = hist.cur_idx)
|
|
hist.last_idx = -1
|
|
m = history_move(s, hist, hist.cur_idx-1, save_idx)
|
|
if m === :ok
|
|
LineEdit.move_input_start(s)
|
|
LineEdit.reset_key_repeats(s) do
|
|
LineEdit.move_line_end(s)
|
|
end
|
|
LineEdit.refresh_line(s)
|
|
elseif m === :skip
|
|
hist.cur_idx -= 1
|
|
history_prev(s, hist, save_idx)
|
|
else
|
|
Terminals.beep(LineEdit.terminal(s))
|
|
end
|
|
end
|
|
|
|
function history_next(s::LineEdit.MIState, hist::REPLHistoryProvider,
|
|
save_idx::Int = hist.cur_idx)
|
|
cur_idx = hist.cur_idx
|
|
max_idx = length(hist.history) + 1
|
|
if cur_idx == max_idx && 0 < hist.last_idx
|
|
# issue #6312
|
|
cur_idx = hist.last_idx
|
|
hist.last_idx = -1
|
|
end
|
|
m = history_move(s, hist, cur_idx+1, save_idx)
|
|
if m === :ok
|
|
LineEdit.move_input_end(s)
|
|
LineEdit.refresh_line(s)
|
|
elseif m === :skip
|
|
hist.cur_idx += 1
|
|
history_next(s, hist, save_idx)
|
|
else
|
|
Terminals.beep(LineEdit.terminal(s))
|
|
end
|
|
end
|
|
|
|
function history_move_prefix(s::LineEdit.PrefixSearchState,
|
|
hist::REPLHistoryProvider,
|
|
prefix::AbstractString,
|
|
backwards::Bool,
|
|
cur_idx = hist.cur_idx)
|
|
cur_response = String(LineEdit.buffer(s))
|
|
# when searching forward, start at last_idx
|
|
if !backwards && hist.last_idx > 0
|
|
cur_idx = hist.last_idx
|
|
end
|
|
hist.last_idx = -1
|
|
max_idx = length(hist.history)+1
|
|
idxs = backwards ? ((cur_idx-1):-1:1) : ((cur_idx+1):max_idx)
|
|
for idx in idxs
|
|
if (idx == max_idx) || (startswith(hist.history[idx], prefix) && (hist.history[idx] != cur_response || hist.modes[idx] != LineEdit.mode(s)))
|
|
m = history_move(s, hist, idx)
|
|
if m === :ok
|
|
if idx == max_idx
|
|
# on resuming the in-progress edit, leave the cursor where the user last had it
|
|
elseif isempty(prefix)
|
|
# on empty prefix search, move cursor to the end
|
|
LineEdit.move_input_end(s)
|
|
else
|
|
# otherwise, keep cursor at the prefix position as a visual cue
|
|
seek(LineEdit.buffer(s), sizeof(prefix))
|
|
end
|
|
LineEdit.refresh_line(s)
|
|
return :ok
|
|
elseif m === :skip
|
|
return history_move_prefix(s,hist,prefix,backwards,idx)
|
|
end
|
|
end
|
|
end
|
|
Terminals.beep(LineEdit.terminal(s))
|
|
end
|
|
history_next_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
|
|
history_move_prefix(s, hist, prefix, false)
|
|
history_prev_prefix(s::LineEdit.PrefixSearchState, hist::REPLHistoryProvider, prefix::AbstractString) =
|
|
history_move_prefix(s, hist, prefix, true)
|
|
|
|
function history_search(hist::REPLHistoryProvider, query_buffer::IOBuffer, response_buffer::IOBuffer,
|
|
backwards::Bool=false, skip_current::Bool=false)
|
|
|
|
qpos = position(query_buffer)
|
|
qpos > 0 || return true
|
|
searchdata = beforecursor(query_buffer)
|
|
response_str = String(response_buffer)
|
|
|
|
# Alright, first try to see if the current match still works
|
|
a = position(response_buffer) + 1 # position is zero-indexed
|
|
b = min(endof(response_str), prevind(response_str, a + sizeof(searchdata))) # ensure that b is valid
|
|
|
|
!skip_current && searchdata == response_str[a:b] && return true
|
|
|
|
searchfunc, searchstart, skipfunc = backwards ? (rsearch, b, prevind) :
|
|
(search, a, nextind)
|
|
skip_current && (searchstart = skipfunc(response_str, searchstart))
|
|
|
|
# Start searching
|
|
# First the current response buffer
|
|
if 1 <= searchstart <= endof(response_str)
|
|
match = searchfunc(response_str, searchdata, searchstart)
|
|
if match != 0:-1
|
|
seek(response_buffer, first(match) - 1)
|
|
return true
|
|
end
|
|
end
|
|
|
|
# Now search all the other buffers
|
|
idxs = backwards ? ((hist.cur_idx-1):-1:1) : ((hist.cur_idx+1):length(hist.history))
|
|
for idx in idxs
|
|
h = hist.history[idx]
|
|
match = searchfunc(h, searchdata)
|
|
if match != 0:-1 && h != response_str && haskey(hist.mode_mapping, hist.modes[idx])
|
|
truncate(response_buffer, 0)
|
|
write(response_buffer, h)
|
|
seek(response_buffer, first(match) - 1)
|
|
hist.cur_idx = idx
|
|
return true
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function history_reset_state(hist::REPLHistoryProvider)
|
|
if hist.cur_idx != length(hist.history) + 1
|
|
hist.last_idx = hist.cur_idx
|
|
hist.cur_idx = length(hist.history) + 1
|
|
end
|
|
end
|
|
LineEdit.reset_state(hist::REPLHistoryProvider) = history_reset_state(hist)
|
|
|
|
function return_callback(s)
|
|
ast = Base.syntax_deprecation_warnings(false) do
|
|
Base.parse_input_line(String(LineEdit.buffer(s)))
|
|
end
|
|
if !isa(ast, Expr) || (ast.head != :continue && ast.head != :incomplete)
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
function find_hist_file()
|
|
filename = ".julia_history"
|
|
if isfile(filename)
|
|
return filename
|
|
elseif haskey(ENV, "JULIA_HISTORY")
|
|
return ENV["JULIA_HISTORY"]
|
|
else
|
|
return joinpath(homedir(), filename)
|
|
end
|
|
end
|
|
|
|
backend(r::AbstractREPL) = r.backendref
|
|
|
|
send_to_backend(ast, backend::REPLBackendRef) = send_to_backend(ast, backend.repl_channel, backend.response_channel)
|
|
function send_to_backend(ast, req, rep)
|
|
put!(req, (ast, 1))
|
|
return take!(rep) # (val, bt)
|
|
end
|
|
|
|
function respond(f, repl, main; pass_empty = false)
|
|
return function do_respond(s, buf, ok)
|
|
if !ok
|
|
return transition(s, :abort)
|
|
end
|
|
line = String(take!(buf))
|
|
if !isempty(line) || pass_empty
|
|
reset(repl)
|
|
local val, bt
|
|
try
|
|
# note: value wrapped carefully here to ensure it doesn't get passed through expand
|
|
response = eval(Main, Expr(:body, Expr(:return, Expr(:call, QuoteNode(f), QuoteNode(line)))))
|
|
val, bt = send_to_backend(response, backend(repl))
|
|
catch err
|
|
val = err
|
|
bt = catch_backtrace()
|
|
end
|
|
if !ends_with_semicolon(line) || bt !== nothing
|
|
print_response(repl, val, bt, true, Base.have_color)
|
|
end
|
|
end
|
|
prepare_next(repl)
|
|
reset_state(s)
|
|
s.current_mode.sticky || transition(s, main)
|
|
end
|
|
end
|
|
|
|
function reset(repl::LineEditREPL)
|
|
raw!(repl.t, false)
|
|
print(repl.t,Base.text_colors[:normal])
|
|
end
|
|
|
|
function prepare_next(repl::LineEditREPL)
|
|
println(terminal(repl))
|
|
end
|
|
|
|
function mode_keymap(julia_prompt)
|
|
AnyDict(
|
|
'\b' => function (s,o...)
|
|
if isempty(s) || position(LineEdit.buffer(s)) == 0
|
|
buf = copy(LineEdit.buffer(s))
|
|
transition(s, julia_prompt) do
|
|
LineEdit.state(s, julia_prompt).input_buffer = buf
|
|
end
|
|
else
|
|
LineEdit.edit_backspace(s)
|
|
end
|
|
end,
|
|
"^C" => function (s,o...)
|
|
LineEdit.move_input_end(s)
|
|
LineEdit.refresh_line(s)
|
|
print(LineEdit.terminal(s), "^C\n\n")
|
|
transition(s, julia_prompt)
|
|
transition(s, :reset)
|
|
LineEdit.refresh_line(s)
|
|
end)
|
|
end
|
|
|
|
repl_filename(repl, hp::REPLHistoryProvider) = "REPL[$(length(hp.history)-hp.start_idx)]"
|
|
repl_filename(repl, hp) = "REPL"
|
|
|
|
const JL_PROMPT_PASTE = Ref(true)
|
|
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v
|
|
|
|
function setup_interface(repl::LineEditREPL; hascolor = repl.hascolor, extra_repl_keymap = Dict{Any,Any}[])
|
|
###
|
|
#
|
|
# This function returns the main interface that describes the REPL
|
|
# functionality, it is called internally by functions that setup a
|
|
# Terminal-based REPL frontend, but if you want to customize your REPL
|
|
# or embed the REPL in another interface, you may call this function
|
|
# directly and append it to your interface.
|
|
#
|
|
# Usage:
|
|
#
|
|
# repl_channel,response_channel = Channel(),Channel()
|
|
# start_repl_backend(repl_channel, response_channel)
|
|
# setup_interface(REPLDisplay(t),repl_channel,response_channel)
|
|
#
|
|
###
|
|
|
|
###
|
|
# We setup the interface in two stages.
|
|
# First, we set up all components (prompt,rsearch,shell,help)
|
|
# Second, we create keymaps with appropriate transitions between them
|
|
# and assign them to the components
|
|
#
|
|
###
|
|
|
|
############################### Stage I ################################
|
|
|
|
# This will provide completions for REPL and help mode
|
|
replc = REPLCompletionProvider()
|
|
|
|
# Set up the main Julia prompt
|
|
julia_prompt = Prompt(JULIA_PROMPT;
|
|
# Copy colors from the prompt object
|
|
prompt_prefix = hascolor ? repl.prompt_color : "",
|
|
prompt_suffix = hascolor ?
|
|
(repl.envcolors ? Base.input_color : repl.input_color) : "",
|
|
keymap_func_data = repl,
|
|
complete = replc,
|
|
on_enter = return_callback)
|
|
|
|
# Setup help mode
|
|
help_mode = Prompt("help?> ",
|
|
prompt_prefix = hascolor ? repl.help_color : "",
|
|
prompt_suffix = hascolor ?
|
|
(repl.envcolors ? Base.input_color : repl.input_color) : "",
|
|
keymap_func_data = repl,
|
|
complete = replc,
|
|
# When we're done transform the entered line into a call to help("$line")
|
|
on_done = respond(Docs.helpmode, repl, julia_prompt))
|
|
|
|
# Set up shell mode
|
|
shell_mode = Prompt("shell> ";
|
|
prompt_prefix = hascolor ? repl.shell_color : "",
|
|
prompt_suffix = hascolor ?
|
|
(repl.envcolors ? Base.input_color : repl.input_color) : "",
|
|
keymap_func_data = repl,
|
|
complete = ShellCompletionProvider(),
|
|
# Transform "foo bar baz" into `foo bar baz` (shell quoting)
|
|
# and pass into Base.repl_cmd for processing (handles `ls` and `cd`
|
|
# special)
|
|
on_done = respond(repl, julia_prompt) do line
|
|
Expr(:call, :(Base.repl_cmd),
|
|
:(Base.cmd_gen($(Base.shell_parse(line)[1]))),
|
|
outstream(repl))
|
|
end)
|
|
|
|
|
|
################################# Stage II #############################
|
|
|
|
# Setup history
|
|
# We will have a unified history for all REPL modes
|
|
hp = REPLHistoryProvider(Dict{Symbol,Any}(:julia => julia_prompt,
|
|
:shell => shell_mode,
|
|
:help => help_mode))
|
|
if repl.history_file
|
|
try
|
|
hist_path = find_hist_file()
|
|
f = open(hist_path, true, true, true, false, false)
|
|
finalizer(replc, replc->close(f))
|
|
hist_from_file(hp, f, hist_path)
|
|
catch e
|
|
print_response(repl, e, catch_backtrace(), true, Base.have_color)
|
|
println(outstream(repl))
|
|
info("Disabling history file for this session.")
|
|
repl.history_file = false
|
|
end
|
|
end
|
|
history_reset_state(hp)
|
|
julia_prompt.hist = hp
|
|
shell_mode.hist = hp
|
|
help_mode.hist = hp
|
|
|
|
julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)
|
|
|
|
|
|
search_prompt, skeymap = LineEdit.setup_search_keymap(hp)
|
|
search_prompt.complete = LatexCompletions()
|
|
|
|
# Canonicalize user keymap input
|
|
if isa(extra_repl_keymap, Dict)
|
|
extra_repl_keymap = [extra_repl_keymap]
|
|
end
|
|
|
|
const repl_keymap = AnyDict(
|
|
';' => function (s,o...)
|
|
if isempty(s) || position(LineEdit.buffer(s)) == 0
|
|
buf = copy(LineEdit.buffer(s))
|
|
transition(s, shell_mode) do
|
|
LineEdit.state(s, shell_mode).input_buffer = buf
|
|
end
|
|
else
|
|
edit_insert(s, ';')
|
|
end
|
|
end,
|
|
'?' => function (s,o...)
|
|
if isempty(s) || position(LineEdit.buffer(s)) == 0
|
|
buf = copy(LineEdit.buffer(s))
|
|
transition(s, help_mode) do
|
|
LineEdit.state(s, help_mode).input_buffer = buf
|
|
end
|
|
else
|
|
edit_insert(s, '?')
|
|
end
|
|
end,
|
|
|
|
# Bracketed Paste Mode
|
|
"\e[200~" => (s,o...)->begin
|
|
input = LineEdit.bracketed_paste(s) # read directly from s until reaching the end-bracketed-paste marker
|
|
sbuffer = LineEdit.buffer(s)
|
|
curspos = position(sbuffer)
|
|
seek(sbuffer, 0)
|
|
shouldeval = (nb_available(sbuffer) == curspos && search(sbuffer, UInt8('\n')) == 0)
|
|
seek(sbuffer, curspos)
|
|
if curspos == 0
|
|
# if pasting at the beginning, strip leading whitespace
|
|
input = lstrip(input)
|
|
end
|
|
if !shouldeval
|
|
# when pasting in the middle of input, just paste in place
|
|
# don't try to execute all the WIP, since that's rather confusing
|
|
# and is often ill-defined how it should behave
|
|
edit_insert(s, input)
|
|
return
|
|
end
|
|
edit_insert(sbuffer, input)
|
|
input = String(take!(sbuffer))
|
|
oldpos = start(input)
|
|
firstline = true
|
|
isprompt_paste = false
|
|
while !done(input, oldpos) # loop until all lines have been executed
|
|
if JL_PROMPT_PASTE[]
|
|
# Check if the next statement starts with "julia> ", in that case
|
|
# skip it. But first skip whitespace
|
|
while input[oldpos] in ('\n', ' ', '\t')
|
|
oldpos = nextind(input, oldpos)
|
|
oldpos >= sizeof(input) && return
|
|
end
|
|
# Check if input line starts with "julia> ", remove it if we are in prompt paste mode
|
|
jl_prompt_len = 7
|
|
if (firstline || isprompt_paste) && (oldpos + jl_prompt_len <= sizeof(input) && input[oldpos:oldpos+jl_prompt_len-1] == JULIA_PROMPT)
|
|
isprompt_paste = true
|
|
oldpos += jl_prompt_len
|
|
# If we are prompt pasting and current statement does not begin with julia> , skip to next line
|
|
elseif isprompt_paste
|
|
while input[oldpos] != '\n'
|
|
oldpos = nextind(input, oldpos)
|
|
oldpos >= sizeof(input) && return
|
|
end
|
|
continue
|
|
end
|
|
end
|
|
ast, pos = Base.syntax_deprecation_warnings(false) do
|
|
Base.parse(input, oldpos, raise=false)
|
|
end
|
|
if (isa(ast, Expr) && (ast.head == :error || ast.head == :continue || ast.head == :incomplete)) ||
|
|
(done(input, pos) && !endswith(input, '\n'))
|
|
# remaining text is incomplete (an error, or parser ran to the end but didn't stop with a newline):
|
|
# Insert all the remaining text as one line (might be empty)
|
|
tail = input[oldpos:end]
|
|
if !firstline
|
|
# strip leading whitespace, but only if it was the result of executing something
|
|
# (avoids modifying the user's current leading wip line)
|
|
tail = lstrip(tail)
|
|
end
|
|
LineEdit.replace_line(s, tail)
|
|
LineEdit.refresh_line(s)
|
|
break
|
|
end
|
|
# get the line and strip leading and trailing whitespace
|
|
line = strip(input[oldpos:prevind(input, pos)])
|
|
if !isempty(line)
|
|
# put the line on the screen and history
|
|
LineEdit.replace_line(s, line)
|
|
LineEdit.commit_line(s)
|
|
# execute the statement
|
|
terminal = LineEdit.terminal(s) # This is slightly ugly but ok for now
|
|
raw!(terminal, false) && disable_bracketed_paste(terminal)
|
|
LineEdit.mode(s).on_done(s, LineEdit.buffer(s), true)
|
|
raw!(terminal, true) && enable_bracketed_paste(terminal)
|
|
end
|
|
oldpos = pos
|
|
firstline = false
|
|
end
|
|
end,
|
|
|
|
# Open the editor at the location of a stackframe
|
|
# This is accessing a global variable that gets set in
|
|
# the show_backtrace function.
|
|
"^Q" => (s, o...) -> begin
|
|
linfos = Base.LAST_BACKTRACE_LINE_INFOS
|
|
str = String(take!(LineEdit.buffer(s)))
|
|
n = tryparse(Int, str)
|
|
isnull(n) && @goto writeback
|
|
n = get(n)
|
|
if n <= 0 || n > length(linfos) || startswith(linfos[n][1], "./REPL")
|
|
@goto writeback
|
|
end
|
|
Base.edit(linfos[n][1], linfos[n][2])
|
|
Base.LineEdit.refresh_line(s)
|
|
return
|
|
@label writeback
|
|
write(Base.LineEdit.buffer(s), str)
|
|
return
|
|
end,
|
|
)
|
|
|
|
prefix_prompt, prefix_keymap = LineEdit.setup_prefix_keymap(hp, julia_prompt)
|
|
|
|
a = Dict{Any,Any}[skeymap, repl_keymap, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
|
|
prepend!(a, extra_repl_keymap)
|
|
|
|
julia_prompt.keymap_dict = LineEdit.keymap(a)
|
|
|
|
mk = mode_keymap(julia_prompt)
|
|
|
|
b = Dict{Any,Any}[skeymap, mk, prefix_keymap, LineEdit.history_keymap, LineEdit.default_keymap, LineEdit.escape_defaults]
|
|
prepend!(b, extra_repl_keymap)
|
|
|
|
shell_mode.keymap_dict = help_mode.keymap_dict = LineEdit.keymap(b)
|
|
|
|
ModalInterface([julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt])
|
|
end
|
|
|
|
function run_frontend(repl::LineEditREPL, backend)
|
|
d = REPLDisplay(repl)
|
|
dopushdisplay = repl.specialdisplay === nothing && !in(d,Base.Multimedia.displays)
|
|
dopushdisplay && pushdisplay(d)
|
|
if !isdefined(repl,:interface)
|
|
interface = repl.interface = setup_interface(repl)
|
|
else
|
|
interface = repl.interface
|
|
end
|
|
repl.backendref = backend
|
|
run_interface(repl.t, interface)
|
|
dopushdisplay && popdisplay(d)
|
|
end
|
|
|
|
if isdefined(Base, :banner_color)
|
|
banner(io, t) = banner(io, hascolor(t))
|
|
banner(io, x::Bool) = print(io, x ? Base.banner_color : Base.banner_plain)
|
|
else
|
|
banner(io,t) = Base.banner(io)
|
|
end
|
|
|
|
## StreamREPL ##
|
|
|
|
mutable struct StreamREPL <: AbstractREPL
|
|
stream::IO
|
|
prompt_color::String
|
|
input_color::String
|
|
answer_color::String
|
|
waserror::Bool
|
|
StreamREPL(stream,pc,ic,ac) = new(stream,pc,ic,ac,false)
|
|
end
|
|
StreamREPL(stream::IO) = StreamREPL(stream, Base.text_colors[:green], Base.input_color(), Base.answer_color())
|
|
run_repl(stream::IO) = run_repl(StreamREPL(stream))
|
|
|
|
outstream(s::StreamREPL) = s.stream
|
|
|
|
answer_color(r::LineEditREPL) = r.envcolors ? Base.answer_color() : r.answer_color
|
|
answer_color(r::StreamREPL) = r.answer_color
|
|
input_color(r::LineEditREPL) = r.envcolors ? Base.input_color() : r.input_color
|
|
input_color(r::StreamREPL) = r.input_color
|
|
|
|
# heuristic function to decide if the presence of a semicolon
|
|
# at the end of the expression was intended for suppressing output
|
|
function ends_with_semicolon(line)
|
|
match = rsearch(line, ';')
|
|
if match != 0
|
|
# state for comment parser, assuming that the `;` isn't in a string or comment
|
|
# so input like ";#" will still thwart this to give the wrong (anti-conservative) answer
|
|
comment = false
|
|
comment_start = false
|
|
comment_close = false
|
|
comment_multi = 0
|
|
for c in line[(match + 1):end]
|
|
if comment_multi > 0
|
|
# handle nested multi-line comments
|
|
if comment_close && c == '#'
|
|
comment_close = false
|
|
comment_multi -= 1
|
|
elseif comment_start && c == '='
|
|
comment_start = false
|
|
comment_multi += 1
|
|
else
|
|
comment_start = (c == '#')
|
|
comment_close = (c == '=')
|
|
end
|
|
elseif comment
|
|
# handle line comments
|
|
if c == '\r' || c == '\n'
|
|
comment = false
|
|
end
|
|
elseif comment_start
|
|
# see what kind of comment this is
|
|
comment_start = false
|
|
if c == '='
|
|
comment_multi = 1
|
|
else
|
|
comment = true
|
|
end
|
|
elseif c == '#'
|
|
# start handling for a comment
|
|
comment_start = true
|
|
else
|
|
# outside of a comment, encountering anything but whitespace
|
|
# means the semi-colon was internal to the expression
|
|
isspace(c) || return false
|
|
end
|
|
end
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
|
|
function run_frontend(repl::StreamREPL, backend::REPLBackendRef)
|
|
have_color = Base.have_color
|
|
banner(repl.stream, have_color)
|
|
d = REPLDisplay(repl)
|
|
dopushdisplay = !in(d,Base.Multimedia.displays)
|
|
dopushdisplay && pushdisplay(d)
|
|
repl_channel, response_channel = backend.repl_channel, backend.response_channel
|
|
while !eof(repl.stream)
|
|
if have_color
|
|
print(repl.stream,repl.prompt_color)
|
|
end
|
|
print(repl.stream, "julia> ")
|
|
if have_color
|
|
print(repl.stream, input_color(repl))
|
|
end
|
|
line = readline(repl.stream, chomp=false)
|
|
if !isempty(line)
|
|
ast = Base.parse_input_line(line)
|
|
if have_color
|
|
print(repl.stream, Base.color_normal)
|
|
end
|
|
put!(repl_channel, (ast, 1))
|
|
val, bt = take!(response_channel)
|
|
if !ends_with_semicolon(line)
|
|
print_response(repl, val, bt, true, have_color)
|
|
end
|
|
end
|
|
end
|
|
# Terminate Backend
|
|
put!(repl_channel, (nothing, -1))
|
|
dopushdisplay && popdisplay(d)
|
|
end
|
|
|
|
function start_repl_server(port)
|
|
listen(port) do server, status
|
|
client = accept(server)
|
|
run_repl(client)
|
|
end
|
|
end
|
|
|
|
end # module
|