Skip to content

Commit

Permalink
REPL: generate hint on a worker thread
Browse files Browse the repository at this point in the history
  • Loading branch information
IanButterworth committed Jan 30, 2025
1 parent c172a64 commit cce3c64
Show file tree
Hide file tree
Showing 2 changed files with 71 additions and 41 deletions.
102 changes: 66 additions & 36 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,12 @@ mutable struct MIState
last_action::Symbol
current_action::Symbol
async_channel::Channel{Function}
line_modify_lock::Base.ReentrantLock
hint_generation_lock::Base.ReentrantLock
n_keys_pressed::Threads.Atomic{Int}
end

MIState(i, mod, c, a, m) = MIState(i, mod, mod, c, a, m, String[], 0, Char[], 0, :none, :none, Channel{Function}())
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(), Threads.Atomic{Int}(0))

const BufferLike = Union{MIState,ModeState,IOBuffer}
const State = Union{MIState,ModeState}
Expand Down Expand Up @@ -400,47 +403,74 @@ function complete_line_named(args...; kwargs...)::Tuple{Vector{NamedCompletion},
end
end

function check_for_hint(s::MIState)
# checks for a hint and shows it if appropriate.
# to allow the user to type even if hint generation is slow, the
# hint is generated on a worker thread, and only shown if the user hasn't
# pressed a key since the hint generation was requested
function check_show_hint(s::MIState)
st = state(s)
lock_clear_hint() = @lock s.line_modify_lock clear_hint(st) && refresh_line(s)
if !options(st).hint_tab_completes || !eof(buffer(st))
# only generate hints if enabled and at the end of the line
# TODO: maybe show hints for insertions at other positions
# Requires making space for them earlier in refresh_multi_line
return clear_hint(st)
lock_clear_hint()
return
end

named_completions, partial, should_complete = try
complete_line_named(st.p.complete, st, s.active_module; hint = true)
catch
@debug "error completing line for hint" exception=current_exceptions()
return clear_hint(st)
end
completions = map(x -> x.completion, named_completions)

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
singlecompletion = length(completions) == 1
p = singlecompletion ? completions[1] : common_prefix(completions)
if singlecompletion || p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
# The completion `p` and the input `partial` may not share the same initial
# characters, for instance when completing to subscripts or superscripts.
# So, in general, make sure that the hint starts at the correct position by
# incrementing its starting position by as many characters as the input.
startind = 1 # index of p from which to start providing the hint
maxind = ncodeunits(p)
for _ in partial
startind = nextind(p, startind)
startind > maxind && break
this_key_i = s.n_keys_pressed[]
next_key_pressed() = s.n_keys_pressed[] > this_key_i
t_completion = Threads.@spawn :default begin
named_completions, partial, should_complete = nothing, nothing, nothing

# only allow one task to generate hints at a time and check around lock
# if the user has pressed a key since the hint was requested, to skip old completions
next_key_pressed() && return
@lock s.hint_generation_lock begin
next_key_pressed() && return
named_completions, partial, should_complete = try
complete_line_named(st.p.complete, st, s.active_module; hint = true)
catch
next_key_pressed() || lock_clear_hint()
return
end
if startind maxind # completion on a complete name returns itself so check that there's something to hint
hint = p[startind:end]
st.hint = hint
return true
end
next_key_pressed() && return

completions = map(x -> x.completion, named_completions)
if isempty(completions)
lock_clear_hint()
return
end
# Don't complete for single chars, given e.g. `x` completes to `xor`
if length(partial) > 1 && should_complete
singlecompletion = length(completions) == 1
p = singlecompletion ? completions[1] : common_prefix(completions)
if singlecompletion || p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
# The completion `p` and the input `partial` may not share the same initial
# characters, for instance when completing to subscripts or superscripts.
# So, in general, make sure that the hint starts at the correct position by
# incrementing its starting position by as many characters as the input.
startind = 1 # index of p from which to start providing the hint
maxind = ncodeunits(p)
for _ in partial
startind = nextind(p, startind)
startind > maxind && break
end
if startind maxind # completion on a complete name returns itself so check that there's something to hint
hint = p[startind:end]
next_key_pressed() && return
@lock s.line_modify_lock begin
state(s).hint = hint
refresh_line(s)
end
return
end
end
end
next_key_pressed() || lock_clear_hint()
end
return clear_hint(st)
Base.errormonitor(t_completion)
return
end

function clear_hint(s::ModeState)
Expand Down Expand Up @@ -2569,7 +2599,7 @@ 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)),
"*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c); check_show_hint(s)),
"^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),
Expand Down Expand Up @@ -2875,10 +2905,9 @@ keymap_data(ms::MIState, m::ModalInterface) = keymap_data(state(ms), mode(ms))

function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_state(term, prompt))
Base.reseteof(term)
l = Base.ReentrantLock()
t1 = Threads.@spawn :interactive while true
wait(s.async_channel)
status = @lock l begin
status = @lock s.line_modify_lock begin
fcn = take!(s.async_channel)
fcn(s)
end
Expand All @@ -2893,7 +2922,8 @@ function prompt!(term::TextTerminal, prompt::ModalInterface, s::MIState = init_s
# and we want to not block typing when the repl task thread is busy
t2 = Threads.@spawn :interactive while true
eof(term) || peek(term) # wait before locking but don't consume
@lock l begin
Threads.atomic_add!(s.n_keys_pressed, 1)
@lock s.line_modify_lock begin
kmap = keymap(s, prompt)
fcn = match_input(kmap, s)
kdata = keymap_data(s, prompt)
Expand Down
10 changes: 5 additions & 5 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1431,7 +1431,7 @@ function setup_interface(
end
else
edit_insert(s, ';')
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
LineEdit.check_show_hint(s)
end
end,
'?' => function (s::MIState,o...)
Expand All @@ -1442,7 +1442,7 @@ function setup_interface(
end
else
edit_insert(s, '?')
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
LineEdit.check_show_hint(s)
end
end,
']' => function (s::MIState,o...)
Expand All @@ -1465,8 +1465,8 @@ function setup_interface(
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))
if !isempty(s)
@invokelatest(LineEdit.check_show_hint(s))
end
break
end
Expand All @@ -1479,7 +1479,7 @@ function setup_interface(
Base.errormonitor(t_replswitch)
else
edit_insert(s, ']')
LineEdit.check_for_hint(s) && LineEdit.refresh_line(s)
LineEdit.check_show_hint(s)
end
end,

Expand Down

0 comments on commit cce3c64

Please sign in to comment.