Skip to content

Commit

Permalink
[WIP] REPL: allow switching contextual module
Browse files Browse the repository at this point in the history
`Main` remains the default module in which REPL expressions
are `eval`ed, but it's possible to switch to a module `Mod` via
`activate_module(Mod)`. The default prompt then indicates this,
e.g. `(Mod) julia> `.
  • Loading branch information
rfourquet committed Oct 4, 2020
1 parent 9392bbe commit e88bb02
Show file tree
Hide file tree
Showing 5 changed files with 87 additions and 54 deletions.
12 changes: 12 additions & 0 deletions base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,18 @@ function run_main_repl(interactive::Bool, quiet::Bool, banner::Bool, history_fil
nothing
end

"""
activate_module(mod::Module=Main)
Set `mod` as the default contextual module in the REPL,
both for evaluating expressions and printing them.
"""
function activate_module(mod::Module=Main)
global active_repl
active_repl.mistate.active_module = mod
nothing
end

# MainInclude exists to hide Main.include and eval from `names(Main)`.
baremodule MainInclude
using ..Base
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,7 @@ export
# misc
atexit,
atreplinit,
activate_module,
exit,
ntuple,

Expand Down
20 changes: 12 additions & 8 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ show(io::IO, x::Prompt) = show(io, string("Prompt(\"", prompt_string(x.prompt),

mutable struct MIState
interface::ModalInterface
active_module::Module
current_mode::TextInterface
aborted::Bool
mode_state::IdDict{TextInterface,ModeState}
Expand All @@ -72,7 +73,7 @@ mutable struct MIState
current_action::Symbol
end

MIState(i, c, a, m) = MIState(i, c, a, m, String[], 0, Char[], 0, :none, :none)
MIState(i, mod, c, a, m) = MIState(i, mod, c, a, m, String[], 0, Char[], 0, :none, :none)

const BufferLike = Union{MIState,ModeState,IOBuffer}
const State = Union{MIState,ModeState}
Expand Down Expand Up @@ -166,7 +167,10 @@ struct EmptyHistoryProvider <: HistoryProvider end

reset_state(::EmptyHistoryProvider) = nothing

complete_line(c::EmptyCompletionProvider, s) = String[], "", true
complete_line(c::EmptyCompletionProvider, s, mod) = String[], "", true

# for Pkg which specializes complete_line with only 2 arguments
complete_line(c::CompletionProvider, s, ::Module) = complete_line(c, s)

terminal(s::IO) = s
terminal(s::PromptState) = s.terminal
Expand Down Expand Up @@ -336,16 +340,16 @@ end
# Prompt Completions
function complete_line(s::MIState)
set_action!(s, :complete_line)
if complete_line(state(s), s.key_repeats)
if complete_line(state(s), s.key_repeats, s.active_module)
return refresh_line(s)
else
beep(s)
return :ignore
end
end

function complete_line(s::PromptState, repeats::Int)
completions, partial, should_complete = complete_line(s.p.complete, s)::Tuple{Vector{String},String,Bool}
function complete_line(s::PromptState, repeats::Int, mod::Module)
completions, partial, should_complete = complete_line(s.p.complete, s, mod)::Tuple{Vector{String},String,Bool}
isempty(completions) && return false
if !should_complete
# should_complete is false for cases where we only want to show
Expand Down Expand Up @@ -1850,8 +1854,8 @@ mode(s::SearchState) = @assert false
mode(s::PrefixSearchState) = s.histprompt.parent_prompt # ::Prompt

# Search Mode completions
function complete_line(s::SearchState, repeats)
completions, partial, should_complete = complete_line(s.histprompt.complete, s)
function complete_line(s::SearchState, repeats, mod::Module)
completions, partial, should_complete = complete_line(s.histprompt.complete, s, mod)
# For now only allow exact completions in search mode
if length(completions) == 1
prev_pos = position(s)
Expand Down Expand Up @@ -2417,7 +2421,7 @@ init_state(terminal, prompt::Prompt) =
#=indent(spaces)=# -1, Threads.SpinLock(), 0.0, -Inf)

function init_state(terminal, m::ModalInterface)
s = MIState(m, m.modes[1], false, IdDict{Any,Any}())
s = MIState(m, Main, m.modes[1], false, IdDict{Any,Any}())
for mode in m.modes
s.mode_state[mode] = init_state(terminal, mode)
end
Expand Down
59 changes: 37 additions & 22 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ const softscope! = softscope

const repl_ast_transforms = Any[softscope] # defaults for new REPL backends

function eval_user_input(@nospecialize(ast), backend::REPLBackend)
function eval_user_input(@nospecialize(ast), backend::REPLBackend, mod::Module)
lasterr = nothing
Base.sigatomic_begin()
while true
Expand All @@ -136,10 +136,10 @@ function eval_user_input(@nospecialize(ast), backend::REPLBackend)
for xf in backend.ast_transforms
ast = Base.invokelatest(xf, ast)
end
value = Core.eval(Main, ast)
value = Core.eval(mod, ast)
backend.in_eval = false
# note: use jl_set_global to make sure value isn't passed through `expand`
ccall(:jl_set_global, Cvoid, (Any, Any, Any), Main, :ans, value)
ccall(:jl_set_global, Cvoid, (Any, Any, Any), mod, :ans, value)
put!(backend.response_channel, (value,false))
end
break
Expand All @@ -163,30 +163,30 @@ end
Deprecated since sync / async behavior cannot be selected
"""
function start_repl_backend(repl_channel::Channel, response_channel::Channel)
function start_repl_backend(repl_channel::Channel, response_channel::Channel; get_module::Function = ()->Main)
# Maintain legacy behavior of asynchronous backend
backend = REPLBackend(repl_channel, response_channel, false)
# Assignment will be made twice, but will be immediately available
backend.backend_task = @async start_repl_backend(backend)
backend.backend_task = @async start_repl_backend(backend; get_module)
return backend
end

"""
start_repl_backend(backend::REPLBackend)
p
Call directly to run backend loop on current Task.
Use @async for run backend on new Task.
Does not return backend until loop is finished.
"""
function start_repl_backend(backend::REPLBackend, @nospecialize(consumer = x -> nothing))
function start_repl_backend(backend::REPLBackend, @nospecialize(consumer = x -> nothing); get_module::Function = ()->Main)
backend.backend_task = Base.current_task()
consumer(backend)
repl_backend_loop(backend)
repl_backend_loop(backend, get_module)
return backend
end

function repl_backend_loop(backend::REPLBackend)
function repl_backend_loop(backend::REPLBackend, get_module::Function)
# include looks at this to determine the relative include path
# nothing means cwd
while true
Expand All @@ -197,7 +197,7 @@ function repl_backend_loop(backend::REPLBackend)
# exit flag
break
end
eval_user_input(ast, backend)
eval_user_input(ast, backend, get_module())
end
return nothing
end
Expand All @@ -210,7 +210,7 @@ end

function display(d::REPLDisplay, mime::MIME"text/plain", x)
with_repl_linfo(d.repl) do io
io = IOContext(io, :limit => true, :module => Main::Module)
io = IOContext(io, :limit => true, :module => active_module(d)::Module)
get(io, :color, false) && write(io, answer_color(d.repl))
if isdefined(d.repl, :options) && isdefined(d.repl.options, :iocontext)
# this can override the :limit property set initially
Expand All @@ -226,7 +226,7 @@ display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x)
function print_response(repl::AbstractREPL, @nospecialize(response), show_value::Bool, have_color::Bool)
repl.waserror = response[2]
with_repl_linfo(repl) do io
io = IOContext(io, :module => Main::Module)
io = IOContext(io, :module => active_module(repl)::Module)
print_response(io, response, show_value, have_color, specialdisplay(repl))
end
return nothing
Expand Down Expand Up @@ -283,6 +283,7 @@ struct REPLBackendRef
response_channel::Channel
end
REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel)

function destroy(ref::REPLBackendRef, state::Task)
if istaskfailed(state) && Base.task_result(state) isa Exception
close(ref.repl_channel, TaskFailedException(state))
Expand Down Expand Up @@ -310,12 +311,13 @@ function run_repl(repl::AbstractREPL, @nospecialize(consumer = x -> nothing); ba
Core.println(Core.stderr, e)
Core.println(Core.stderr, catch_backtrace())
end
get_module = () -> active_module(repl)
if backend_on_current_task
t = @async run_frontend(repl, backend_ref)
Base._wait2(t, cleanup)
start_repl_backend(backend, consumer)
start_repl_backend(backend, consumer; get_module)
else
t = @async start_repl_backend(backend, consumer)
t = @async start_repl_backend(backend, consumer; get_module)
Base._wait2(t, cleanup)
run_frontend(repl, backend_ref)
end
Expand Down Expand Up @@ -426,28 +428,32 @@ LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
false, false, false, envcolors
)

active_module(repl::LineEditREPL) = repl.mistate.active_module
active_module(::AbstractREPL) = Main
active_module(d::REPLDisplay) = active_module(d.repl)

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::PromptState)
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module)
partial = beforecursor(s.input_buffer)
full = LineEdit.input_string(s)
ret, range, should_complete = completions(full, lastindex(partial))
ret, range, should_complete = completions(full, lastindex(partial), mod)
return unique!(map(completion_text, ret)), partial[range], should_complete
end

function complete_line(c::ShellCompletionProvider, s::PromptState)
function complete_line(c::ShellCompletionProvider, s::PromptState, ::Module)
# 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, lastindex(partial))
return unique!(map(completion_text, ret)), partial[range], should_complete
end

function complete_line(c::LatexCompletions, s::PromptState)
function complete_line(c::LatexCompletions, s::PromptState, ::Module)
partial = beforecursor(LineEdit.buffer(s))
full = LineEdit.input_string(s)
ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
Expand Down Expand Up @@ -840,6 +846,15 @@ repl_filename(repl, hp) = "REPL"
const JL_PROMPT_PASTE = Ref(true)
enable_promptpaste(v::Bool) = JL_PROMPT_PASTE[] = v

function contextual_prompt(repl::LineEditREPL, prompt::Union{String,Function})
function ()
mod = repl.mistate.active_module
prefix = mod == Main ? "" : string('(', mod, ") ")
pr = prompt isa String ? prompt : prompt()
prefix * pr
end
end

setup_interface(
repl::LineEditREPL;
# those keyword arguments may be deprecated eventually in favor of the Options mechanism
Expand Down Expand Up @@ -884,7 +899,7 @@ function setup_interface(
replc = REPLCompletionProvider()

# Set up the main Julia prompt
julia_prompt = Prompt(JULIA_PROMPT;
julia_prompt = Prompt(contextual_prompt(repl, JULIA_PROMPT);
# Copy colors from the prompt object
prompt_prefix = hascolor ? repl.prompt_color : "",
prompt_suffix = hascolor ?
Expand All @@ -894,15 +909,15 @@ function setup_interface(
on_enter = return_callback)

# Setup help mode
help_mode = Prompt("help?> ",
help_mode = Prompt(contextual_prompt(repl, "help?> "),
prompt_prefix = hascolor ? repl.help_color : "",
prompt_suffix = hascolor ?
(repl.envcolors ? Base.input_color : repl.input_color) : "",
repl = repl,
complete = replc,
# When we're done transform the entered line into a call to helpmode function
on_done = respond(line::String->helpmode(outstream(repl), line), repl, julia_prompt,
pass_empty=true, suppress_on_semicolon=false))
on_done = respond(line::String->helpmode(outstream(repl), line, repl.mistate.active_module),
repl, julia_prompt, pass_empty=true, suppress_on_semicolon=false))


# Set up shell mode
Expand Down
Loading

0 comments on commit e88bb02

Please sign in to comment.