From e88bb023754b3d023096ebace70d76a438804683 Mon Sep 17 00:00:00 2001 From: Rafael Fourquet Date: Sun, 17 Nov 2019 14:21:34 +0100 Subject: [PATCH] [WIP] REPL: allow switching contextual module `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> `. --- base/client.jl | 12 ++++++++ base/exports.jl | 1 + stdlib/REPL/src/LineEdit.jl | 20 ++++++++----- stdlib/REPL/src/REPL.jl | 59 +++++++++++++++++++++++-------------- stdlib/REPL/src/docview.jl | 49 +++++++++++++++--------------- 5 files changed, 87 insertions(+), 54 deletions(-) diff --git a/base/client.jl b/base/client.jl index 25fb605d729e3e..e8d8613077810c 100644 --- a/base/client.jl +++ b/base/client.jl @@ -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 diff --git a/base/exports.jl b/base/exports.jl index 2c0c628eec866b..c81d6dacb17d77 100644 --- a/base/exports.jl +++ b/base/exports.jl @@ -778,6 +778,7 @@ export # misc atexit, atreplinit, + activate_module, exit, ntuple, diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index 9375d42d7d605c..7cef75da12c2da 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -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} @@ -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} @@ -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 @@ -336,7 +340,7 @@ 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) @@ -344,8 +348,8 @@ function complete_line(s::MIState) 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 @@ -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) @@ -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 diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index 1e70738bb215fe..e5dc8c4c6a0090 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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 @@ -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)) @@ -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 @@ -426,20 +428,24 @@ 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) @@ -447,7 +453,7 @@ function complete_line(c::ShellCompletionProvider, s::PromptState) 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] @@ -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 @@ -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 ? @@ -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 diff --git a/stdlib/REPL/src/docview.jl b/stdlib/REPL/src/docview.jl index 3d4dddc72176c6..eb1287d641f3b4 100644 --- a/stdlib/REPL/src/docview.jl +++ b/stdlib/REPL/src/docview.jl @@ -20,12 +20,12 @@ using Unicode: normalize ## Help mode ## # This is split into helpmode and _helpmode to easier unittest _helpmode -helpmode(io::IO, line::AbstractString) = :($REPL.insert_hlines($io, $(REPL._helpmode(io, line)))) -helpmode(line::AbstractString) = helpmode(stdout, line) +helpmode(io::IO, line::AbstractString, mod::Module) = :($REPL.insert_hlines($io, $(REPL._helpmode(io, line, mod)))) +helpmode(line::AbstractString, mod::Module) = helpmode(stdout, line, mod) const extended_help_on = Ref{Any}(nothing) -function _helpmode(io::IO, line::AbstractString) +function _helpmode(io::IO, line::AbstractString, mod::Module=Main) line = strip(line) ternary_operator_help = (line == "?" || line == "?:") if startswith(line, '?') && !ternary_operator_help @@ -64,7 +64,7 @@ function _helpmode(io::IO, line::AbstractString) end # the following must call repl(io, expr) via the @repl macro # so that the resulting expressions are evaluated in the Base.Docs namespace - :($REPL.@repl $io $expr $brief) + :($REPL.@repl $io $expr $brief $mod) end _helpmode(line::AbstractString) = _helpmode(stdout, line) @@ -306,18 +306,18 @@ end # repl search and completions for help -function repl_search(io::IO, s::Union{Symbol,String}) +function repl_search(io::IO, s::Union{Symbol,String}, mod::Module) pre = "search:" print(io, pre) - printmatches(io, s, doc_completions(s), cols = _displaysize(io)[2] - length(pre)) + printmatches(io, s, doc_completions(s, mod), cols = _displaysize(io)[2] - length(pre)) println(io, "\n") end -repl_search(s) = repl_search(stdout, s) +repl_search(s) = repl_search(stdout, s, mod) -function repl_corrections(io::IO, s) +function repl_corrections(io::IO, s, mod::Module) print(io, "Couldn't find ") printstyled(io, s, '\n', color=:cyan) - print_correction(io, s) + print_correction(io, s, mod) end repl_corrections(s) = repl_corrections(stdout, s) @@ -361,27 +361,28 @@ function repl_latex(io::IO, s::String) end repl_latex(s::String) = repl_latex(stdout, s) -macro repl(ex, brief::Bool=false) repl(ex; brief=brief) end -macro repl(io, ex, brief) repl(io, ex; brief=brief) end +macro repl(ex, brief::Bool=false, mod::Module=Main) repl(ex; brief, mod) end +macro repl(io, ex, brief, mod) repl(io, ex; brief, mod) end -function repl(io::IO, s::Symbol; brief::Bool=true) +function repl(io::IO, s::Symbol; brief::Bool=true, mod::Module=Main) str = string(s) quote repl_latex($io, $str) - repl_search($io, $str) - $(if !isdefined(Main, s) && !haskey(keywords, s) && !Base.isoperator(s) - :(repl_corrections($io, $str)) + repl_search($io, $str, $mod) + $(if !isdefined(mod, s) && !haskey(keywords, s) && !Base.isoperator(s) + :(repl_corrections($io, $str, $mod)) end) $(_repl(s, brief)) end end isregex(x) = isexpr(x, :macrocall, 3) && x.args[1] === Symbol("@r_str") && !isempty(x.args[3]) -repl(io::IO, ex::Expr; brief::Bool=true) = isregex(ex) ? :(apropos($io, $ex)) : _repl(ex, brief) -repl(io::IO, str::AbstractString; brief::Bool=true) = :(apropos($io, $str)) -repl(io::IO, other; brief::Bool=true) = esc(:(@doc $other)) + +repl(io::IO, ex::Expr; brief::Bool=true, mod::Module=Main) = isregex(ex) ? :(apropos($io, $ex, $mod)) : _repl(ex, brief) +repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main) = :(apropos($io, $str, $mod)) +repl(io::IO, other; brief::Bool=true, mod::Module=Main) = esc(:(@doc $other)) #repl(io::IO, other) = lookup_doc(other) # TODO -repl(x; brief::Bool=true) = repl(stdout, x; brief=brief) +repl(x; brief::Bool=true, mod::Module=Main) = repl(stdout, x; brief, mod) function _repl(x, brief::Bool=true) if isexpr(x, :call) @@ -598,8 +599,8 @@ end print_joined_cols(args...; cols::Int = _displaysize(stdout)[2]) = print_joined_cols(stdout, args...; cols=cols) -function print_correction(io::IO, word::String) - cors = levsort(word, accessible(Main)) +function print_correction(io::IO, word::String, mod::Module) + cors = levsort(word, accessible(mod)) pre = "Perhaps you meant " print(io, pre) print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre)) @@ -607,7 +608,7 @@ function print_correction(io::IO, word::String) return end -print_correction(word) = print_correction(stdout, word) +print_correction(word) = print_correction(stdout, word, Main) # Completion data @@ -621,8 +622,8 @@ accessible(mod::Module) = map(names, moduleusings(mod))...; collect(keys(Base.Docs.keywords))] |> unique |> filtervalid -doc_completions(name) = fuzzysort(name, accessible(Main)) -doc_completions(name::Symbol) = doc_completions(string(name)) +doc_completions(name, mod::Module=Main) = fuzzysort(name, accessible(mod)) +doc_completions(name::Symbol, mod::Module=Main) = doc_completions(string(name), mod) # Searching and apropos