Skip to content

Commit 0b9525b

Browse files
REPL: generate tab completion hints on a worker thread to not block typing (#57192)
1 parent dbe19e4 commit 0b9525b

File tree

2 files changed

+79
-41
lines changed

2 files changed

+79
-41
lines changed

stdlib/REPL/src/LineEdit.jl

+74-36
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,12 @@ mutable struct MIState
7777
last_action::Symbol
7878
current_action::Symbol
7979
async_channel::Channel{Function}
80+
line_modify_lock::Base.ReentrantLock
81+
hint_generation_lock::Base.ReentrantLock
82+
n_keys_pressed::Int
8083
end
8184

82-
MIState(i, mod, c, a, m) = MIState(i, mod, mod, c, a, m, String[], 0, Char[], 0, :none, :none, Channel{Function}())
85+
MIState(i, mod, c, a, m) = MIState(i, mod, mod, c, a, m, String[], 0, Char[], 0, :none, :none, Channel{Function}(), Base.ReentrantLock(), Base.ReentrantLock(), 0)
8386

8487
const BufferLike = Union{MIState,ModeState,IOBuffer}
8588
const State = Union{MIState,ModeState}
@@ -400,47 +403,82 @@ function complete_line_named(args...; kwargs...)::Tuple{Vector{NamedCompletion},
400403
end
401404
end
402405

403-
function check_for_hint(s::MIState)
406+
# checks for a hint and shows it if appropriate.
407+
# to allow the user to type even if hint generation is slow, the
408+
# hint is generated on a worker thread, and only shown if the user hasn't
409+
# pressed a key since the hint generation was requested
410+
function check_show_hint(s::MIState)
404411
st = state(s)
412+
413+
this_key_i = s.n_keys_pressed
414+
next_key_pressed() = @lock s.line_modify_lock s.n_keys_pressed > this_key_i
415+
function lock_clear_hint()
416+
@lock s.line_modify_lock begin
417+
next_key_pressed() || s.aborted || clear_hint(st) && refresh_line(s)
418+
end
419+
end
420+
405421
if !options(st).hint_tab_completes || !eof(buffer(st))
406422
# only generate hints if enabled and at the end of the line
407423
# TODO: maybe show hints for insertions at other positions
408424
# Requires making space for them earlier in refresh_multi_line
409-
return clear_hint(st)
425+
lock_clear_hint()
426+
return
410427
end
411-
412-
named_completions, partial, should_complete = try
413-
complete_line_named(st.p.complete, st, s.active_module; hint = true)
414-
catch
415-
@debug "error completing line for hint" exception=current_exceptions()
416-
return clear_hint(st)
417-
end
418-
completions = map(x -> x.completion, named_completions)
419-
420-
isempty(completions) && return clear_hint(st)
421-
# Don't complete for single chars, given e.g. `x` completes to `xor`
422-
if length(partial) > 1 && should_complete
423-
singlecompletion = length(completions) == 1
424-
p = singlecompletion ? completions[1] : common_prefix(completions)
425-
if singlecompletion || p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
426-
# The completion `p` and the input `partial` may not share the same initial
427-
# characters, for instance when completing to subscripts or superscripts.
428-
# So, in general, make sure that the hint starts at the correct position by
429-
# incrementing its starting position by as many characters as the input.
430-
startind = 1 # index of p from which to start providing the hint
431-
maxind = ncodeunits(p)
432-
for _ in partial
433-
startind = nextind(p, startind)
434-
startind > maxind && break
428+
t_completion = Threads.@spawn :default begin
429+
named_completions, partial, should_complete = nothing, nothing, nothing
430+
431+
# only allow one task to generate hints at a time and check around lock
432+
# if the user has pressed a key since the hint was requested, to skip old completions
433+
next_key_pressed() && return
434+
@lock s.hint_generation_lock begin
435+
next_key_pressed() && return
436+
named_completions, partial, should_complete = try
437+
complete_line_named(st.p.complete, st, s.active_module; hint = true)
438+
catch
439+
lock_clear_hint()
440+
return
435441
end
436-
if startind maxind # completion on a complete name returns itself so check that there's something to hint
437-
hint = p[startind:end]
438-
st.hint = hint
439-
return true
442+
end
443+
next_key_pressed() && return
444+
445+
completions = map(x -> x.completion, named_completions)
446+
if isempty(completions)
447+
lock_clear_hint()
448+
return
449+
end
450+
# Don't complete for single chars, given e.g. `x` completes to `xor`
451+
if length(partial) > 1 && should_complete
452+
singlecompletion = length(completions) == 1
453+
p = singlecompletion ? completions[1] : common_prefix(completions)
454+
if singlecompletion || p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
455+
# The completion `p` and the input `partial` may not share the same initial
456+
# characters, for instance when completing to subscripts or superscripts.
457+
# So, in general, make sure that the hint starts at the correct position by
458+
# incrementing its starting position by as many characters as the input.
459+
startind = 1 # index of p from which to start providing the hint
460+
maxind = ncodeunits(p)
461+
for _ in partial
462+
startind = nextind(p, startind)
463+
startind > maxind && break
464+
end
465+
if startind maxind # completion on a complete name returns itself so check that there's something to hint
466+
hint = p[startind:end]
467+
next_key_pressed() && return
468+
@lock s.line_modify_lock begin
469+
if !s.aborted
470+
state(s).hint = hint
471+
refresh_line(s)
472+
end
473+
end
474+
return
475+
end
440476
end
441477
end
478+
lock_clear_hint()
442479
end
443-
return clear_hint(st)
480+
Base.errormonitor(t_completion)
481+
return
444482
end
445483

446484
function clear_hint(s::ModeState)
@@ -2569,7 +2607,7 @@ AnyDict(
25692607
"^_" => (s::MIState,o...)->edit_undo!(s),
25702608
"\e_" => (s::MIState,o...)->edit_redo!(s),
25712609
# Show hints at what tab complete would do by default
2572-
"*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c); check_for_hint(s) && refresh_line(s)),
2610+
"*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c); check_show_hint(s)),
25732611
"^U" => (s::MIState,o...)->edit_kill_line_backwards(s),
25742612
"^K" => (s::MIState,o...)->edit_kill_line_forwards(s),
25752613
"^Y" => (s::MIState,o...)->edit_yank(s),
@@ -2875,10 +2913,9 @@ keymap_data(ms::MIState, m::ModalInterface) = keymap_data(state(ms), mode(ms))
28752913

28762914
function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_state(term, prompt))
28772915
Base.reseteof(term)
2878-
l = Base.ReentrantLock()
28792916
t1 = Threads.@spawn :interactive while true
28802917
wait(s.async_channel)
2881-
status = @lock l begin
2918+
status = @lock s.line_modify_lock begin
28822919
fcn = take!(s.async_channel)
28832920
fcn(s)
28842921
end
@@ -2893,7 +2930,8 @@ function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_s
28932930
# and we want to not block typing when the repl task thread is busy
28942931
t2 = Threads.@spawn :interactive while true
28952932
eof(term) || peek(term) # wait before locking but don't consume
2896-
@lock l begin
2933+
@lock s.line_modify_lock begin
2934+
s.n_keys_pressed += 1
28972935
kmap = keymap(s, prompt)
28982936
fcn = match_input(kmap, s)
28992937
kdata = keymap_data(s, prompt)

stdlib/REPL/src/REPL.jl

+5-5
Original file line numberDiff line numberDiff line change
@@ -1431,7 +1431,7 @@ function setup_interface(
14311431
end
14321432
else
14331433
edit_insert(s, ';')
1434-
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
1434+
LineEdit.check_show_hint(s)
14351435
end
14361436
end,
14371437
'?' => function (s::MIState,o...)
@@ -1442,7 +1442,7 @@ function setup_interface(
14421442
end
14431443
else
14441444
edit_insert(s, '?')
1445-
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
1445+
LineEdit.check_show_hint(s)
14461446
end
14471447
end,
14481448
']' => function (s::MIState,o...)
@@ -1465,8 +1465,8 @@ function setup_interface(
14651465
transition(s, mode) do
14661466
LineEdit.state(s, mode).input_buffer = buf
14671467
end
1468-
if !isempty(s) && @invokelatest(LineEdit.check_for_hint(s))
1469-
@invokelatest(LineEdit.refresh_line(s))
1468+
if !isempty(s)
1469+
@invokelatest(LineEdit.check_show_hint(s))
14701470
end
14711471
break
14721472
end
@@ -1479,7 +1479,7 @@ function setup_interface(
14791479
Base.errormonitor(t_replswitch)
14801480
else
14811481
edit_insert(s, ']')
1482-
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
1482+
LineEdit.check_show_hint(s)
14831483
end
14841484
end,
14851485

0 commit comments

Comments
 (0)