Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reimplement dummy pkg prompt as standard prompt #54674

Merged
merged 1 commit into from
Jun 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,10 @@ mutable struct MIState
key_repeats::Int
last_action::Symbol
current_action::Symbol
async_channel::Channel
end

MIState(i, mod, c, a, m) = MIState(i, mod, 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, Channel())

const BufferLike = Union{MIState,ModeState,IOBuffer}
const State = Union{MIState,ModeState}
Expand Down Expand Up @@ -2309,7 +2310,7 @@ keymap_data(state, ::Union{HistoryPrompt, PrefixHistoryPrompt}) = state

Base.isempty(s::PromptState) = s.input_buffer.size == 0

on_enter(s::PromptState) = s.p.on_enter(s)
on_enter(s::MIState) = state(s).p.on_enter(s)

move_input_start(s::BufferLike) = (seek(buffer(s), 0); nothing)
move_input_end(buf::IOBuffer) = (seekend(buf); nothing)
Expand Down Expand Up @@ -2829,7 +2830,11 @@ function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_s
old_state = mode(s)
while true
kmap = keymap(s, prompt)
fcn = match_input(kmap, s)
waitany((
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

waitany() isn't in 1.11 unfortunately, so this can't be easily backported unless #53341 is backported too.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Keno @KristofferC I think it's reasonable to backport waitany so I've added the label.

@async(eof(term) || peek(term, Char)),
@async(wait(s.async_channel)),
), throw=true)
fcn = isempty(s.async_channel) ? match_input(kmap, s) : take!(s.async_channel)
kdata = keymap_data(s, prompt)
s.current_action = :unknown # if the to-be-run action doesn't update this field,
# :unknown will be recorded in the last_action field
Expand Down
117 changes: 76 additions & 41 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ function check_for_missing_packages_and_run_hooks(ast)
mods = modules_to_be_loaded(ast)
filter!(mod -> isnothing(Base.identify_package(String(mod))), mods) # keep missing modules
if !isempty(mods)
isempty(install_packages_hooks) && Base.require_stdlib(Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg"))
isempty(install_packages_hooks) && load_pkg()
for f in install_packages_hooks
Base.invokelatest(f, mods) && return
end
Expand Down Expand Up @@ -575,6 +575,7 @@ mutable struct LineEditREPL <: AbstractREPL
answer_color::String
shell_color::String
help_color::String
pkg_color::String
history_file::Bool
in_shell::Bool
in_help::Bool
Expand All @@ -587,13 +588,13 @@ mutable struct LineEditREPL <: AbstractREPL
interface::ModalInterface
backendref::REPLBackendRef
frontend_task::Task
function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,in_help,envcolors)
function LineEditREPL(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,in_help,envcolors)
opts = Options()
opts.hascolor = hascolor
if !hascolor
opts.beep_colors = [""]
end
new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,history_file,in_shell,
new(t,hascolor,prompt_color,input_color,answer_color,shell_color,help_color,pkg_color,history_file,in_shell,
in_help,envcolors,false,nothing, opts, nothing, Tuple{String,Int}[])
end
end
Expand All @@ -610,6 +611,7 @@ LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
hascolor ? Base.answer_color() : "",
hascolor ? Base.text_colors[:red] : "",
hascolor ? Base.text_colors[:yellow] : "",
hascolor ? Base.text_colors[:blue] : "",
false, false, false, envcolors
)

Expand Down Expand Up @@ -1080,6 +1082,20 @@ setup_interface(
extra_repl_keymap::Any = repl.options.extra_keymap
) = setup_interface(repl, hascolor, extra_repl_keymap)

const Pkg_pkgid = Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg")
const Pkg_REPLExt_pkgid = Base.PkgId(Base.UUID("ceef7b17-42e7-5b1c-81d4-4cc4a2494ccf"), "REPLExt")

function load_pkg()
@lock Base.require_lock begin
REPLExt = Base.require_stdlib(Pkg_pkgid, "REPLExt")
# require_stdlib does not guarantee that the `__init__` of the package is done when loading is done async
# but we need to wait for the repl mode to be set up
lock = get(Base.package_locks, Pkg_REPLExt_pkgid.uuid, nothing)
lock !== nothing && wait(lock[2])
return REPLExt
end
end

# This non keyword method can be precompiled which is important
function setup_interface(
repl::LineEditREPL,
Expand Down Expand Up @@ -1155,14 +1171,43 @@ function setup_interface(
end,
sticky = true)

# Set up dummy Pkg mode that will be replaced once Pkg is loaded
# use 6 dots to occupy the same space as the most likely "@v1.xx" env name
dummy_pkg_mode = Prompt("(......) $PKG_PROMPT",
prompt_prefix = hascolor ? repl.pkg_color : "",
prompt_suffix = hascolor ?
(repl.envcolors ? Base.input_color : repl.input_color) : "",
repl = repl,
complete = LineEdit.EmptyCompletionProvider(),
on_done = respond(line->nothing, repl, julia_prompt),
on_enter = function (s::MIState)
# This is hit when the user tries to execute a command before the real Pkg mode has been
# switched to. Ok to do this even if Pkg is loading on the other task because of the loading lock.
REPLExt = load_pkg()
if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
for mode in repl.interface.modes
if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
# pkg mode
buf = copy(LineEdit.buffer(s))
transition(s, mode) do
LineEdit.state(s, mode).input_buffer = buf
end
end
end
end
return true
end,
sticky = true)


################################# Stage II #############################

# Setup history
# We will have a unified history for all REPL modes
hp = REPLHistoryProvider(Dict{Symbol,Prompt}(:julia => julia_prompt,
:shell => shell_mode,
:help => help_mode))
:help => help_mode,
:pkg => dummy_pkg_mode))
if repl.history_file
try
hist_path = find_hist_file()
Expand All @@ -1185,6 +1230,7 @@ function setup_interface(
julia_prompt.hist = hp
shell_mode.hist = hp
help_mode.hist = hp
dummy_pkg_mode.hist = hp

julia_prompt.on_done = respond(x->Base.parse_input_line(x,filename=repl_filename(repl,hp)), repl, julia_prompt)

Expand Down Expand Up @@ -1225,47 +1271,36 @@ function setup_interface(
end,
']' => function (s::MIState,o...)
if isempty(s) || position(LineEdit.buffer(s)) == 0
# print a dummy pkg prompt while Pkg loads
LineEdit.clear_line(LineEdit.terminal(s))
# use 6 .'s here because its the same width as the most likely `@v1.xx` env name
print(LineEdit.terminal(s), styled"{blue,bold:({gray:......}) pkg> }")
pkg_mode = nothing
transition_finished = false
iolock = Base.ReentrantLock() # to avoid race between tasks reading stdin & input buffer
# spawn Pkg load to avoid blocking typing during loading. Typing will block if only 1 thread
buf = copy(LineEdit.buffer(s))
transition(s, dummy_pkg_mode) do
LineEdit.state(s, dummy_pkg_mode).input_buffer = buf
end
# load Pkg on another thread if available so that typing in the dummy Pkg prompt
# isn't blocked, but instruct the main REPL task to do the transition via s.async_channel
t_replswitch = Threads.@spawn begin
pkgid = Base.PkgId(Base.UUID("44cfe95a-1eb2-52ea-b672-e2afdf69b78f"), "Pkg")
REPLExt = Base.require_stdlib(pkgid, "REPLExt")
REPLExt = load_pkg()
if REPLExt isa Module && isdefined(REPLExt, :PkgCompletionProvider)
for mode in repl.interface.modes
if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
pkg_mode = mode
break
put!(s.async_channel,
function (s::MIState, _)
LineEdit.mode(s) === dummy_pkg_mode || return :ok
for mode in repl.interface.modes
if mode isa LineEdit.Prompt && mode.complete isa REPLExt.PkgCompletionProvider
buf = copy(LineEdit.buffer(s))
transition(s, mode) do
LineEdit.state(s, mode).input_buffer = buf
end
if !isempty(s) && @invokelatest(LineEdit.check_for_hint(s))
@invokelatest(LineEdit.refresh_line(s))
end
break
end
end
return :ok
end
end
end
if pkg_mode !== nothing
@lock iolock begin
buf = copy(LineEdit.buffer(s))
transition(s, pkg_mode) do
LineEdit.state(s, pkg_mode).input_buffer = buf
end
if !isempty(s)
@invokelatest(LineEdit.check_for_hint(s)) && @invokelatest(LineEdit.refresh_line(s))
end
transition_finished = true
end
)
end
end
Base.errormonitor(t_replswitch)
# while loading just accept all keys, no keymap functionality
while !istaskdone(t_replswitch)
# wait but only take if task is still running
peek(stdin, Char)
@lock iolock begin
transition_finished || edit_insert(s, read(stdin, Char))
end
end
else
edit_insert(s, ']')
end
Expand Down Expand Up @@ -1448,9 +1483,9 @@ function setup_interface(
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)
shell_mode.keymap_dict = help_mode.keymap_dict = dummy_pkg_mode.keymap_dict = LineEdit.keymap(b)

allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, search_prompt, prefix_prompt]
allprompts = LineEdit.TextInterface[julia_prompt, shell_mode, help_mode, dummy_pkg_mode, search_prompt, prefix_prompt]
return ModalInterface(allprompts)
end

Expand Down
10 changes: 6 additions & 4 deletions stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -500,8 +500,9 @@ for prompt = ["TestΠ", () -> randstring(rand(1:10))]
repl_mode = repl.interface.modes[1]
shell_mode = repl.interface.modes[2]
help_mode = repl.interface.modes[3]
histp = repl.interface.modes[4]
prefix_mode = repl.interface.modes[5]
pkg_mode = repl.interface.modes[4]
histp = repl.interface.modes[5]
prefix_mode = repl.interface.modes[6]

hp = REPL.REPLHistoryProvider(Dict{Symbol,Any}(:julia => repl_mode,
:shell => shell_mode,
Expand Down Expand Up @@ -1588,8 +1589,9 @@ for prompt = ["TestΠ", () -> randstring(rand(1:10))]
repl_mode = repl.interface.modes[1]
shell_mode = repl.interface.modes[2]
help_mode = repl.interface.modes[3]
histp = repl.interface.modes[4]
prefix_mode = repl.interface.modes[5]
pkg_mode = repl.interface.modes[4]
histp = repl.interface.modes[5]
prefix_mode = repl.interface.modes[6]

hp = REPL.REPLHistoryProvider(Dict{Symbol,Any}(:julia => repl_mode,
:shell => shell_mode,
Expand Down