Skip to content

Commit

Permalink
REPL: allow switching contextual module (#33872)
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
`REPL.activate_module` or the keybinding Alt-m. The default prompt then indicates this,
e.g. `(Mod) julia> `.


Co-authored-by: KristofferC <[email protected]>
  • Loading branch information
rfourquet and KristofferC authored Jun 6, 2022
1 parent bd8dbc3 commit 803f90d
Show file tree
Hide file tree
Showing 13 changed files with 276 additions and 87 deletions.
4 changes: 4 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion base/Enums.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 6 additions & 6 deletions base/client.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions base/docs/Docs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion base/docs/bindings.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
28 changes: 17 additions & 11 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, ".")
Expand Down Expand Up @@ -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, ".")
Expand Down Expand Up @@ -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, ".")
Expand Down Expand Up @@ -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


Expand Down
6 changes: 3 additions & 3 deletions stdlib/InteractiveUtils/src/InteractiveUtils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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, ""),]
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
59 changes: 59 additions & 0 deletions stdlib/REPL/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Alt-m> # using the keybinding to change module
(Core) julia>
(Core) julia> Main<Alt-m> # 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.
Expand Down
64 changes: 57 additions & 7 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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}
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -343,16 +348,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 @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -2437,6 +2486,7 @@ const prefix_history_keymap = merge!(
end,
# match escape sequences for pass through
"^x*" => "*",
"\em*" => "*",
"\e*" => "*",
"\e[*" => "*",
"\eO*" => "*",
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 803f90d

Please sign in to comment.