From 77c28ab286f48afe2512e2ae7f7310b87ca3345e Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Sun, 9 Jun 2024 03:01:10 -0400 Subject: [PATCH] Reimplement dummy pkg prompt as standard prompt (#54674) --- stdlib/REPL/src/LineEdit.jl | 11 +++- stdlib/REPL/src/REPL.jl | 117 +++++++++++++++++++++++------------- stdlib/REPL/test/repl.jl | 10 +-- 3 files changed, 90 insertions(+), 48 deletions(-) diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index 1003dc0e51d84..4af7ea187d9e9 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -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} @@ -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) @@ -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(( + @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 diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index a39b44d17f9a1..34af995c9b162 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -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 @@ -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 @@ -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 @@ -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 ) @@ -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, @@ -1155,6 +1171,34 @@ 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 ############################# @@ -1162,7 +1206,8 @@ function setup_interface( # 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() @@ -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) @@ -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 @@ -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 diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 09ff10e770e84..b3a62dbdb4ee5 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -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, @@ -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,