diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index 01daef9d66899..01508d50b22fb 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -381,7 +381,16 @@ function check_for_hint(s::MIState) # Requires making space for them earlier in refresh_multi_line return clear_hint(st) end - completions, partial, should_complete = complete_line(st.p.complete, st, s.active_module)::Tuple{Vector{String},String,Bool} + # spawn a new task for hint completion to avoid blocking the main thread + # if the completion task doesn't finish quickly, abandon hinting and return early + completion_task = Threads.@spawn :default complete_line(st.p.complete, st, s.active_module) + done_quickly = false + completion_timer = Timer(0.01) do _ + done_quickly = istaskdone(completion_task) + end + wait(completion_timer); close(completion_timer) + done_quickly || return clear_hint(st) # TODO kill `completion_task`? + completions, partial, should_complete = fetch(completion_task)::Tuple{Vector{String},String,Bool} isempty(completions) && return clear_hint(st) # Don't complete for single chars, given e.g. `x` completes to `xor` if length(partial) > 1 && should_complete @@ -2395,7 +2404,8 @@ end # jump_spaces: if cursor is on a ' ', move it to the first non-' ' char on the right # if `delete_trailing`, ignore trailing ' ' by deleting them function edit_tab(s::MIState, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces) - tab_should_complete(s) && return complete_line(s) + # tab complete on a separate thread (if available) to avoid blocking the main thread + tab_should_complete(s) && return Threads.@spawn :default complete_line(s) set_action!(s, :edit_insert_tab) push_undo(s) edit_insert_tab(buffer(s), jump_spaces, delete_trailing) || pop_undo(s) @@ -2503,7 +2513,10 @@ AnyDict( "^_" => (s::MIState,o...)->edit_undo!(s), "\e_" => (s::MIState,o...)->edit_redo!(s), # Show hints at what tab complete would do by default - "*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c); check_for_hint(s) && refresh_line(s)), + "*" => function (s::MIState,data,c::StringLike) + edit_insert(s, c) + check_for_hint(s) && refresh_line(s) + end, "^U" => (s::MIState,o...)->edit_kill_line_backwards(s), "^K" => (s::MIState,o...)->edit_kill_line_forwards(s), "^Y" => (s::MIState,o...)->edit_yank(s), diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index da5d20270c5b6..f1e58606b5185 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -646,6 +646,10 @@ function resolve_toplevel_symbols!(src::Core.CodeInfo, mod::Module) return src end +# the lock to avoid concurrent inference from multiple completion tasks, which could happen +# if user types fast rapidly (otherwise end up with corrupt inference states) +const REPL_INFERENCE_LOCK = ReentrantLock() + # lower `ex` and run type inference on the resulting top-level expression function repl_eval_ex(@nospecialize(ex), context_module::Module; limit_aggressive_inference::Bool=false) if (isexpr(ex, :toplevel) || isexpr(ex, :tuple)) && !isempty(ex.args) @@ -662,6 +666,17 @@ function repl_eval_ex(@nospecialize(ex), context_module::Module; limit_aggressiv end lwr isa Expr || return Const(lwr) # `ex` is literal isexpr(lwr, :thunk) || return nothing # lowered to `Expr(:error, ...)` or similar + trylock(REPL_INFERENCE_LOCK) || return nothing + result = try + repl_infer_ex(lwr, context_module, limit_aggressive_inference) + finally + unlock(REPL_INFERENCE_LOCK) + end + result === Union{} && return nothing # for whatever reason, callers expect this as the Bottom and/or Top type instead + return result +end + +function repl_infer_ex(lwr::Expr, context_module::Module, limit_aggressive_inference::Bool=false) src = lwr.args[1]::Core.CodeInfo # construct top-level `MethodInstance` @@ -680,9 +695,7 @@ function repl_eval_ex(@nospecialize(ex), context_module::Module; limit_aggressiv # potential invalidations of `Core.Compiler` methods. Base.invoke_in_world(COMPLETION_WORLD[], CC.typeinf, interp, frame) - result = frame.result.result - result === Union{} && return nothing # for whatever reason, callers expect this as the Bottom and/or Top type instead - return result + return frame.result.result end # `COMPLETION_WORLD[]` will be initialized within `__init__`