diff --git a/NEWS.md b/NEWS.md index 69e4d7af7e781..3bc140e77843c 100644 --- a/NEWS.md +++ b/NEWS.md @@ -115,6 +115,10 @@ Standard library changes * `Meta-e` now opens the current input in an editor. The content (if modified) will be executed upon existing the editor. +* The contextual module which is active at the REPL can be changed (it is `Main` by default), + via the `REPL.activate(::Module)` function or via typing the module in the REPL and pressing + the keybinding Alt-m ([#33872]). + #### SparseArrays #### Test diff --git a/base/Enums.jl b/base/Enums.jl index 0b990721be717..413c880fcd3f2 100644 --- a/base/Enums.jl +++ b/base/Enums.jl @@ -36,7 +36,7 @@ Base.print(io::IO, x::Enum) = print(io, _symbol(x)) function Base.show(io::IO, x::Enum) sym = _symbol(x) if !(get(io, :compact, false)::Bool) - from = get(io, :module, Main) + from = get(io, :module, Base.active_module()) def = typeof(x).name.module if from === nothing || !Base.isvisible(sym, def, from) show(io, def) diff --git a/base/client.jl b/base/client.jl index 84317a80d84aa..66d7ffc3d2135 100644 --- a/base/client.jl +++ b/base/client.jl @@ -373,21 +373,21 @@ _atreplinit(repl) = invokelatest(__atreplinit, repl) # The REPL stdlib hooks into Base using this Ref const REPL_MODULE_REF = Ref{Module}() -function load_InteractiveUtils() +function load_InteractiveUtils(mod::Module=Main) # load interactive-only libraries - if !isdefined(Main, :InteractiveUtils) + if !isdefined(mod, :InteractiveUtils) try let InteractiveUtils = require(PkgId(UUID(0xb77e0a4c_d291_57a0_90e8_8db25a27a240), "InteractiveUtils")) - Core.eval(Main, :(const InteractiveUtils = $InteractiveUtils)) - Core.eval(Main, :(using .InteractiveUtils)) + Core.eval(mod, :(const InteractiveUtils = $InteractiveUtils)) + Core.eval(mod, :(using .InteractiveUtils)) return InteractiveUtils end catch ex - @warn "Failed to import InteractiveUtils into module Main" exception=(ex, catch_backtrace()) + @warn "Failed to import InteractiveUtils into module $mod" exception=(ex, catch_backtrace()) end return nothing end - return getfield(Main, :InteractiveUtils) + return getfield(mod, :InteractiveUtils) end # run the requested sort of evaluation loop on stdio diff --git a/base/docs/Docs.jl b/base/docs/Docs.jl index b84b3ee8d55f4..2c52d8f921ef2 100644 --- a/base/docs/Docs.jl +++ b/base/docs/Docs.jl @@ -236,8 +236,10 @@ function doc!(__module__::Module, b::Binding, str::DocStr, @nospecialize sig = U if haskey(m.docs, sig) # We allow for docstrings to be updated, but print a warning since it is possible # that over-writing a docstring *may* have been accidental. The warning - # is suppressed for symbols in Main, for interactive use (#23011). - __module__ === Main || @warn "Replacing docs for `$b :: $sig` in module `$(__module__)`" + # is suppressed for symbols in Main (or current active module), + # for interactive use (#23011). + __module__ === Base.active_module() || + @warn "Replacing docs for `$b :: $sig` in module `$(__module__)`" else # The ordering of docstrings for each Binding is defined by the order in which they # are initially added. Replacing a specific docstring does not change it's ordering. diff --git a/base/docs/bindings.jl b/base/docs/bindings.jl index d96154f05fcb1..6095d52a28e5a 100644 --- a/base/docs/bindings.jl +++ b/base/docs/bindings.jl @@ -33,7 +33,7 @@ macro var(x) end function Base.show(io::IO, b::Binding) - if b.mod === Main + if b.mod === Base.active_module() print(io, b.var) else print(io, b.mod, '.', Base.isoperator(b.var) ? ":" : "", b.var) diff --git a/base/show.jl b/base/show.jl index 113d3ca786a05..d4ce6886a2197 100644 --- a/base/show.jl +++ b/base/show.jl @@ -482,13 +482,18 @@ function _show_default(io::IO, @nospecialize(x)) print(io,')') end +active_module()::Module = isdefined(Base, :active_repl) && isdefined(Base.active_repl, :mistate) && Base.active_repl.mistate !== nothing ? + Base.active_repl.mistate.active_module : + Main + # Check if a particular symbol is exported from a standard library module function is_exported_from_stdlib(name::Symbol, mod::Module) !isdefined(mod, name) && return false orig = getfield(mod, name) while !(mod === Base || mod === Core) + activemod = active_module() parent = parentmodule(mod) - if mod === Main || mod === parent || parent === Main + if mod === activemod || mod === parent || parent === activemod return false end mod = parent @@ -506,7 +511,8 @@ function show_function(io::IO, f::Function, compact::Bool, fallback::Function) print(io, mt.name) elseif isdefined(mt, :module) && isdefined(mt.module, mt.name) && getfield(mt.module, mt.name) === f - if is_exported_from_stdlib(mt.name, mt.module) || mt.module === Main + mod = active_module() + if is_exported_from_stdlib(mt.name, mt.module) || mt.module === mod show_sym(io, mt.name) else print(io, mt.module, ".") @@ -700,9 +706,9 @@ end function show_typealias(io::IO, name::GlobalRef, x::Type, env::SimpleVector, wheres::Vector) if !(get(io, :compact, false)::Bool) # Print module prefix unless alias is visible from module passed to - # IOContext. If :module is not set, default to Main. nothing can be used - # to force printing prefix. - from = get(io, :module, Main) + # IOContext. If :module is not set, default to Main (or current active module). + # nothing can be used to force printing prefix. + from = get(io, :module, active_module()) if (from === nothing || !isvisible(name.name, name.mod, from)) show(io, name.mod) print(io, ".") @@ -1016,9 +1022,9 @@ function show_type_name(io::IO, tn::Core.TypeName) quo = false if !(get(io, :compact, false)::Bool) # Print module prefix unless type is visible from module passed to - # IOContext If :module is not set, default to Main. nothing can be used - # to force printing prefix - from = get(io, :module, Main) + # IOContext If :module is not set, default to Main (or current active module). + # nothing can be used to force printing prefix + from = get(io, :module, active_module()) if isdefined(tn, :module) && (from === nothing || !isvisible(sym, tn.module, from)) show(io, tn.module) print(io, ".") @@ -2772,9 +2778,9 @@ MyStruct ``` """ function dump(arg; maxdepth=DUMP_DEFAULT_MAXDEPTH) - # this is typically used interactively, so default to being in Main - mod = get(stdout, :module, Main) - dump(IOContext(stdout, :limit => true, :module => mod), arg; maxdepth=maxdepth) + # this is typically used interactively, so default to being in Main (or current active module) + mod = get(stdout, :module, active_module()) + dump(IOContext(stdout::IO, :limit => true, :module => mod), arg; maxdepth=maxdepth) end diff --git a/stdlib/InteractiveUtils/src/InteractiveUtils.jl b/stdlib/InteractiveUtils/src/InteractiveUtils.jl index 6c742660ca73c..ad295345fabfd 100644 --- a/stdlib/InteractiveUtils/src/InteractiveUtils.jl +++ b/stdlib/InteractiveUtils/src/InteractiveUtils.jl @@ -34,7 +34,7 @@ The memory consumption estimate is an approximate lower bound on the size of the - `sortby` : the column to sort results by. Options are `:name` (default), `:size`, and `:summary`. - `minsize` : only includes objects with size at least `minsize` bytes. Defaults to `0`. """ -function varinfo(m::Module=Main, pattern::Regex=r""; all::Bool = false, imported::Bool = false, sortby::Symbol = :name, recursive::Bool = false, minsize::Int=0) +function varinfo(m::Module=Base.active_module(), pattern::Regex=r""; all::Bool = false, imported::Bool = false, sortby::Symbol = :name, recursive::Bool = false, minsize::Int=0) sortby in (:name, :size, :summary) || throw(ArgumentError("Unrecognized `sortby` value `:$sortby`. Possible options are `:name`, `:size`, and `:summary`")) rows = Vector{Any}[] workqueue = [(m, ""),] @@ -45,7 +45,7 @@ function varinfo(m::Module=Main, pattern::Regex=r""; all::Bool = false, imported continue end value = getfield(m2, v) - isbuiltin = value === Base || value === Main || value === Core + isbuiltin = value === Base || value === Base.active_module() || value === Core if recursive && !isbuiltin && isa(value, Module) && value !== m2 && nameof(value) === v && parentmodule(value) === m2 push!(workqueue, (value, "$prep$v.")) end @@ -75,7 +75,7 @@ function varinfo(m::Module=Main, pattern::Regex=r""; all::Bool = false, imported return Markdown.MD(Any[Markdown.Table(map(r->r[1:3], rows), Symbol[:l, :r, :l])]) end -varinfo(pat::Regex; kwargs...) = varinfo(Main, pat, kwargs...) +varinfo(pat::Regex; kwargs...) = varinfo(Base.active_module(), pat; kwargs...) """ versioninfo(io::IO=stdout; verbose::Bool=false) diff --git a/stdlib/REPL/docs/src/index.md b/stdlib/REPL/docs/src/index.md index 1d98bb829f785..203f377c9ba63 100644 --- a/stdlib/REPL/docs/src/index.md +++ b/stdlib/REPL/docs/src/index.md @@ -557,6 +557,65 @@ ENV["JULIA_WARN_COLOR"] = :yellow ENV["JULIA_INFO_COLOR"] = :cyan ``` + +## Changing the contextual module which is active at the REPL + +When entering expressions at the REPL, they are by default evaluated in the `Main` module; + +```julia-repl +julia> @__MODULE__ +Main +``` + +It is possible to change this contextual module via the function +`REPL.activate(m)` where `m` is a `Module` or by typing the module in the REPL +and pressing the keybinding Alt-m (the cursor must be on the module name). The +active module is shown in the prompt: + +```julia-repl +julia> using REPL + +julia> REPL.activate(Base) + +(Base) julia> @__MODULE__ +Base + +(Base) julia> using REPL # Need to load REPL into Base module to use it + +(Base) julia> REPL.activate(Main) + +julia> + +julia> Core # using the keybinding to change module + +(Core) julia> + +(Core) julia> Main # going back to Main via keybinding + +julia> +``` + +Functions that take an optional module argument often defaults to the REPL +context module. As an example, calling `varinfo()` will show the variables of +the current active module: + +```julia-repl +julia> module CustomMod + export var, f + var = 1 + f(x) = x^2 + end; + +julia> REPL.activate(CustomMod) + +(Main.CustomMod) julia> varinfo() + name size summary + ––––––––– ––––––– –––––––––––––––––––––––––––––––––– + CustomMod Module + f 0 bytes f (generic function with 1 method) + var 8 bytes Int64 +``` + ## TerminalMenus TerminalMenus is a submodule of the Julia REPL and enables small, low-profile interactive menus in the terminal. diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index fe6813c168a13..b30a1d816a83f 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -63,6 +63,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} @@ -74,7 +75,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} @@ -177,6 +178,10 @@ reset_state(::EmptyHistoryProvider) = nothing complete_line(c::EmptyCompletionProvider, s) = String[], "", true +# complete_line can be specialized for only two arguments, when the active module +# doesn't matter (e.g. Pkg does this) +complete_line(c::CompletionProvider, s, ::Module) = complete_line(c, s) + terminal(s::IO) = s terminal(s::PromptState) = s.terminal @@ -343,7 +348,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) @@ -351,8 +356,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 @@ -1359,6 +1364,49 @@ function edit_input(s, f = (filename, line, column) -> InteractiveUtils.edit(fil end end +# return the identifier under the cursor, possibly with other words concatenated +# to it with dots (e.g. "A.B.C" in "X; A.B.C*3", if the cursor is between "A" and "C") +function current_word_with_dots(buf::IOBuffer) + pos = position(buf) + while true + char_move_word_right(buf) + if eof(buf) || peek(buf, Char) != '.' + break + end + end + pend = position(buf) + while true + char_move_word_left(buf) + p = position(buf) + p == 0 && break + seek(buf, p-1) + if peek(buf, Char) != '.' + seek(buf, p) + break + end + end + pbegin = position(buf) + word = pend > pbegin ? + String(buf.data[pbegin+1:pend]) : + "" + seek(buf, pos) + word +end + +current_word_with_dots(s::MIState) = current_word_with_dots(buffer(s)) + +function activate_module(s::MIState) + word = current_word_with_dots(s); + isempty(word) && return beep(s) + try + mod = Base.Core.eval(Base.active_module(), Base.Meta.parse(word)) + REPL.activate(mod) + edit_clear(s) + catch + beep(s) + end +end + history_prev(::EmptyHistoryProvider) = ("", false) history_next(::EmptyHistoryProvider) = ("", false) history_first(::EmptyHistoryProvider) = ("", false) @@ -1980,8 +2028,8 @@ setmodifiers!(p::Prompt, m::Modifiers) = setmodifiers!(p.complete, m) setmodifiers!(c) = nothing # 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) @@ -2401,6 +2449,7 @@ AnyDict( "\el" => (s::MIState,o...)->edit_lower_case(s), "\ec" => (s::MIState,o...)->edit_title_case(s), "\ee" => (s::MIState,o...) -> edit_input(s), + "\em" => (s::MIState, o...) -> activate_module(s) ) const history_keymap = AnyDict( @@ -2437,6 +2486,7 @@ const prefix_history_keymap = merge!( end, # match escape sequences for pass through "^x*" => "*", + "\em*" => "*", "\e*" => "*", "\e[*" => "*", "\eO*" => "*", @@ -2555,7 +2605,7 @@ init_state(terminal, prompt::Prompt) = #=indent(spaces)=# -1, Threads.SpinLock(), 0.0, -Inf, nothing) 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 df35c433d9f78..4a5246301cf43 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -133,7 +133,7 @@ const repl_ast_transforms = Any[softscope] # defaults for new REPL backends # to e.g. install packages on demand const install_packages_hooks = Any[] -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 @@ -149,9 +149,9 @@ 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 - setglobal!(Main, :ans, value) + setglobal!(mod, :ans, value) put!(backend.response_channel, Pair{Any, Bool}(value, false)) end break @@ -210,11 +210,12 @@ end Deprecated since sync / async behavior cannot be selected """ -function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any}) +function start_repl_backend(repl_channel::Channel{Any}, response_channel::Channel{Any} + ; 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 @@ -226,14 +227,14 @@ end 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 @@ -244,7 +245,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 @@ -258,7 +259,7 @@ end function display(d::REPLDisplay, mime::MIME"text/plain", x) x = Ref{Any}(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 @@ -274,7 +275,7 @@ display(d::REPLDisplay, x) = display(d, MIME("text/plain"), x) function print_response(repl::AbstractREPL, 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 @@ -335,6 +336,7 @@ struct REPLBackendRef response_channel::Channel{Any} end REPLBackendRef(backend::REPLBackend) = REPLBackendRef(backend.repl_channel, backend.response_channel) + function destroy(ref::REPLBackendRef, state::Task) if istaskfailed(state) close(ref.repl_channel, TaskFailedException(state)) @@ -362,13 +364,14 @@ 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) errormonitor(t) 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) errormonitor(t) Base._wait2(t, cleanup) run_frontend(repl, backend_ref) @@ -484,17 +487,33 @@ mutable struct REPLCompletionProvider <: CompletionProvider modifiers::LineEdit.Modifiers end REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers()) + mutable struct ShellCompletionProvider <: CompletionProvider end struct LatexCompletions <: CompletionProvider end +active_module(repl::LineEditREPL) = repl.mistate === nothing ? Main : repl.mistate.active_module +active_module(::AbstractREPL) = Main +active_module(d::REPLDisplay) = active_module(d.repl) + setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m +""" + activate(mod::Module=Main) + +Set `mod` as the default contextual module in the REPL, +both for evaluating expressions and printing them. +""" +function activate(mod::Module=Main) + Base.active_repl.mistate.active_module = mod + Base.load_InteractiveUtils(mod) + nothing +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), Main, c.modifiers.shift) + ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift) c.modifiers = LineEdit.Modifiers() return unique!(map(completion_text, ret)), partial[range], should_complete end @@ -906,6 +925,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 = active_module(repl) + 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 @@ -950,7 +978,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 ? @@ -960,15 +988,15 @@ function setup_interface( on_enter = return_callback) # Setup help mode - help_mode = Prompt(HELP_PROMPT, + 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 @@ -1025,10 +1053,9 @@ function setup_interface( search_prompt, skeymap = LineEdit.setup_search_keymap(hp) search_prompt.complete = LatexCompletions() - jl_prompt_len = length(JULIA_PROMPT) - pkg_prompt_len = length(PKG_PROMPT) shell_prompt_len = length(SHELL_PROMPT) help_prompt_len = length(HELP_PROMPT) + jl_prompt_regex = r"^(?:\(.+\) )?julia> " pkg_prompt_regex = r"^(?:\(.+\) )?pkg> " # Canonicalize user keymap input @@ -1095,30 +1122,32 @@ function setup_interface( oldpos = nextind(input, oldpos) oldpos >= sizeof(input) && return end + substr = SubString(input, oldpos) # Check if input line starts with "julia> ", remove it if we are in prompt paste mode - if (firstline || isprompt_paste) && startswith(SubString(input, oldpos), JULIA_PROMPT) + if (firstline || isprompt_paste) && startswith(substr, jl_prompt_regex) + detected_jl_prompt = match(jl_prompt_regex, substr).match isprompt_paste = true - oldpos += jl_prompt_len - curr_prompt_len = jl_prompt_len + curr_prompt_len = sizeof(detected_jl_prompt) + oldpos += curr_prompt_len transition(s, julia_prompt) pasting_help = false # Check if input line starts with "pkg> " or "(...) pkg> ", remove it if we are in prompt paste mode and switch mode - elseif (firstline || isprompt_paste) && startswith(SubString(input, oldpos), pkg_prompt_regex) - detected_pkg_prompt = match(pkg_prompt_regex, SubString(input, oldpos)).match + elseif (firstline || isprompt_paste) && startswith(substr, pkg_prompt_regex) + detected_pkg_prompt = match(pkg_prompt_regex, substr).match isprompt_paste = true curr_prompt_len = sizeof(detected_pkg_prompt) oldpos += curr_prompt_len Base.active_repl.interface.modes[1].keymap_dict[']'](s, o...) pasting_help = false # Check if input line starts with "shell> ", remove it if we are in prompt paste mode and switch mode - elseif (firstline || isprompt_paste) && startswith(SubString(input, oldpos), SHELL_PROMPT) + elseif (firstline || isprompt_paste) && startswith(substr, SHELL_PROMPT) isprompt_paste = true oldpos += shell_prompt_len curr_prompt_len = shell_prompt_len transition(s, shell_mode) pasting_help = false # Check if input line starts with "help?> ", remove it if we are in prompt paste mode and switch mode - elseif (firstline || isprompt_paste) && startswith(SubString(input, oldpos), HELP_PROMPT) + elseif (firstline || isprompt_paste) && startswith(substr, HELP_PROMPT) isprompt_paste = true oldpos += help_prompt_len curr_prompt_len = help_prompt_len diff --git a/stdlib/REPL/src/docview.jl b/stdlib/REPL/src/docview.jl index fe55ea6b128af..5d8478c9ae42e 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=Main) = :($REPL.insert_hlines($io, $(REPL._helpmode(io, line, mod)))) +helpmode(line::AbstractString, mod::Module=Main) = 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,9 +64,9 @@ 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) +_helpmode(line::AbstractString, mod::Module=Main) = _helpmode(stdout, line, mod) # Print vertical lines along each docstring if there are multiple docs function insert_hlines(io::IO, docs) @@ -369,21 +369,23 @@ end quote_spaces(x) = any(isspace, x) ? "'" * x * "'" : x -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, map(quote_spaces, doc_completions(s)), cols = _displaysize(io)[2] - length(pre)) + printmatches(io, s, map(quote_spaces, doc_completions(s, mod)), cols = _displaysize(io)[2] - length(pre)) println(io, "\n") end -repl_search(s) = repl_search(stdout, s) -function repl_corrections(io::IO, s) +# TODO: document where this is used +repl_search(s, mod::Module) = repl_search(stdout, s, mod) + +function repl_corrections(io::IO, s, mod::Module) print(io, "Couldn't find ") quot = any(isspace, s) ? "'" : "" print(io, quot) printstyled(io, s, color=:cyan) print(io, quot, '\n') - print_correction(io, s) + print_correction(io, s, mod) end repl_corrections(s) = repl_corrections(stdout, s) @@ -460,27 +462,28 @@ function repl_latex(io::IO, s0::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)) : _repl(ex, brief) +repl(io::IO, str::AbstractString; brief::Bool=true, mod::Module=Main) = :(apropos($io, $str)) +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) @@ -697,8 +700,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 = map(quote_spaces, levsort(word, accessible(Main))) +function print_correction(io::IO, word::String, mod::Module) + cors = map(quote_spaces, levsort(word, accessible(mod))) pre = "Perhaps you meant " print(io, pre) print_joined_cols(io, cors, ", ", " or "; cols = _displaysize(io)[2] - length(pre)) @@ -706,7 +709,8 @@ function print_correction(io::IO, word::String) return end -print_correction(word) = print_correction(stdout, word) +# TODO: document where this is used +print_correction(word, mod::Module) = print_correction(stdout, word, mod) # Completion data @@ -720,8 +724,8 @@ accessible(mod::Module) = map(names, moduleusings(mod))...; collect(keys(Base.Docs.keywords))] |> unique |> filtervalid -function doc_completions(name) - res = fuzzysort(name, accessible(Main)) +function doc_completions(name, mod::Module=Main) + res = fuzzysort(name, accessible(mod)) # to insert an entry like `raw""` for `"@raw_str"` in `res` ms = match.(r"^@(.*?)_str$", res) @@ -733,7 +737,7 @@ function doc_completions(name) end res end -doc_completions(name::Symbol) = doc_completions(string(name)) +doc_completions(name::Symbol) = doc_completions(string(name), mod) # Searching and apropos diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 5495c77ad5b72..6e4132aaab1cd 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -478,6 +478,7 @@ for prompt = ["TestΠ", () -> randstring(rand(1:10))] # Some manual setup s = LineEdit.init_state(repl.t, repl.interface) + repl.mistate = s LineEdit.edit_insert(s, "wip") # LineEdit functions related to history @@ -1096,7 +1097,34 @@ fake_repl() do stdin_write, stdout_read, repl Base.wait(repltask) end -help_result(line) = Base.eval(REPL._helpmode(IOBuffer(), line)) +# test activate_module +fake_repl() do stdin_write, stdout_read, repl + repl.history_file = false + repl.interface = REPL.setup_interface(repl) + repl.mistate = LineEdit.init_state(repl.t, repl.interface) + + repltask = @async begin + REPL.run_repl(repl) + end + + write(stdin_write, "(123, Base.Fix1)\n") + @test occursin("julia> ", split(readline(stdout_read), "Base.Fix1")[2]) + @test occursin("(123, Base.Fix1)", readline(stdout_read)) + readline(stdout_read) + + repl.mistate.active_module = Base # simulate activate_module(Base) + write(stdin_write, "(456, Base.Fix2)\n") + @test occursin("(Base) julia> ", split(readline(stdout_read), "Base.Fix2")[2]) + # ".Base" prefix not shown here + @test occursin("(456, Fix2)", readline(stdout_read)) + readline(stdout_read) + + # Close REPL ^D + write(stdin_write, '\x04') + Base.wait(repltask) +end + +help_result(line, mod::Module=Base) = mod.eval(REPL._helpmode(IOBuffer(), line)) # Docs.helpmode tests: we test whether the correct expressions are being generated here, # rather than complete integration with Julia's REPL mode system. @@ -1139,6 +1167,13 @@ end # Issue #40563 @test occursin("does not exist", sprint(show, help_result(".."))) +# test that helpmode is sensitive to contextual module +@test occursin("No documentation found", sprint(show, help_result("Fix2", Main))) +@test occursin("A type representing a partially-applied version", # exact string may change + sprint(show, help_result("Base.Fix2", Main))) +@test occursin("A type representing a partially-applied version", # exact string may change + sprint(show, help_result("Fix2", Base))) + # Issue #25930 diff --git a/stdlib/REPL/test/replcompletions.jl b/stdlib/REPL/test/replcompletions.jl index f156100b1df47..6dc9d9b8b2883 100644 --- a/stdlib/REPL/test/replcompletions.jl +++ b/stdlib/REPL/test/replcompletions.jl @@ -7,7 +7,7 @@ using REPL @testset "Check symbols previously not shown by REPL.doc_completions()" begin symbols = ["?","=","[]","[","]","{}","{","}",";","","'","&&","||","julia","Julia","new","@var_str"] for i in symbols - @test i ∈ REPL.doc_completions(i) + @test i ∈ REPL.doc_completions(i, Main) end end let ex = quote