Skip to content

Commit ce5fa71

Browse files
Add repl tab complete hints while typing (#51229)
1 parent 572fa50 commit ce5fa71

File tree

5 files changed

+112
-6
lines changed

5 files changed

+112
-6
lines changed

NEWS.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ Standard library changes
4848

4949
#### REPL
5050

51+
* Tab complete hints now show in lighter text while typing in the repl. To disable
52+
set `Base.active_repl.options.hint_tab_completes = false` ([#51229])
53+
5154
#### SuiteSparse
5255

5356

stdlib/REPL/docs/src/index.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -312,7 +312,7 @@ Users should refer to `LineEdit.jl` to discover the available actions on key inp
312312

313313
## Tab completion
314314

315-
In both the Julian and help modes of the REPL, one can enter the first few characters of a function
315+
In the Julian, pkg and help modes of the REPL, one can enter the first few characters of a function
316316
or type and then press the tab key to get a list all matches:
317317

318318
```julia-repl
@@ -334,6 +334,12 @@ julia> mapfold[TAB]
334334
mapfoldl mapfoldr
335335
```
336336

337+
When a single complete tab-complete result is available a hint of the completion will show in a lighter color.
338+
This can be disabled via `Base.active_repl.options.hint_tab_completes = false`.
339+
340+
!!! compat "Julia 1.11"
341+
Tab-complete hinting was added in Julia 1.11
342+
337343
Like other components of the REPL, the search is case-sensitive:
338344

339345
```julia-repl

stdlib/REPL/src/LineEdit.jl

Lines changed: 53 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ mutable struct PromptState <: ModeState
9797
p::Prompt
9898
input_buffer::IOBuffer
9999
region_active::Symbol # :shift or :mark or :off
100+
hint::Union{String,Nothing}
100101
undo_buffers::Vector{IOBuffer}
101102
undo_idx::Int
102103
ias::InputAreaState
@@ -361,7 +362,7 @@ function show_completions(s::PromptState, completions::Vector{String})
361362
end
362363
end
363364

364-
# Prompt Completions
365+
# Prompt Completions & Hints
365366
function complete_line(s::MIState)
366367
set_action!(s, :complete_line)
367368
if complete_line(state(s), s.key_repeats, s.active_module)
@@ -372,6 +373,36 @@ function complete_line(s::MIState)
372373
end
373374
end
374375

376+
function check_for_hint(s::MIState)
377+
st = state(s)
378+
options(st).hint_tab_completes || return false
379+
completions, partial, should_complete = complete_line(st.p.complete, st, s.active_module)::Tuple{Vector{String},String,Bool}
380+
if should_complete
381+
if length(completions) == 1
382+
hint = only(completions)[sizeof(partial)+1:end]
383+
if !isempty(hint) # completion on a complete name returns itself so check that there's something to hint
384+
st.hint = hint
385+
return true
386+
end
387+
elseif length(completions) > 1
388+
p = common_prefix(completions)
389+
if p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
390+
hint = p[sizeof(partial)+1:end]
391+
if !isempty(hint)
392+
st.hint = hint
393+
return true
394+
end
395+
end
396+
end
397+
end
398+
if !isnothing(st.hint)
399+
st.hint = "" # don't set to nothing here. That will be done in `maybe_show_hint`
400+
return true
401+
else
402+
return false
403+
end
404+
end
405+
375406
function complete_line(s::PromptState, repeats::Int, mod::Module)
376407
completions, partial, should_complete = complete_line(s.p.complete, s, mod)::Tuple{Vector{String},String,Bool}
377408
isempty(completions) && return false
@@ -432,12 +463,29 @@ prompt_string(p::Prompt) = prompt_string(p.prompt)
432463
prompt_string(s::AbstractString) = s
433464
prompt_string(f::Function) = Base.invokelatest(f)
434465

466+
function maybe_show_hint(s::PromptState)
467+
isa(s.hint, String) || return nothing
468+
# The hint being "" then nothing is used to first clear a previous hint, then skip printing the hint
469+
# the clear line cannot be printed each time because it breaks column movement
470+
if isempty(s.hint)
471+
print(terminal(s), "\e[0K") # clear remainder of line which had a hint
472+
s.hint = nothing
473+
else
474+
Base.printstyled(terminal(s), s.hint, color=:light_black)
475+
cmove_left(terminal(s), textwidth(s.hint))
476+
s.hint = "" # being "" signals to do one clear line remainder to clear the hint next time if still empty
477+
end
478+
return nothing
479+
end
480+
435481
function refresh_multi_line(s::PromptState; kw...)
436482
if s.refresh_wait !== nothing
437483
close(s.refresh_wait)
438484
s.refresh_wait = nothing
439485
end
440-
refresh_multi_line(terminal(s), s; kw...)
486+
r = refresh_multi_line(terminal(s), s; kw...)
487+
maybe_show_hint(s)
488+
return r
441489
end
442490
refresh_multi_line(s::ModeState; kw...) = refresh_multi_line(terminal(s), s; kw...)
443491
refresh_multi_line(termbuf::TerminalBuffer, s::ModeState; kw...) = refresh_multi_line(termbuf, terminal(s), s; kw...)
@@ -2424,8 +2472,8 @@ AnyDict(
24242472
"\e\n" => "\e\r",
24252473
"^_" => (s::MIState,o...)->edit_undo!(s),
24262474
"\e_" => (s::MIState,o...)->edit_redo!(s),
2427-
# Simply insert it into the buffer by default
2428-
"*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c)),
2475+
# Show hints at what tab complete would do by default
2476+
"*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c); check_for_hint(s) && refresh_line(s)),
24292477
"^U" => (s::MIState,o...)->edit_kill_line_backwards(s),
24302478
"^K" => (s::MIState,o...)->edit_kill_line_forwards(s),
24312479
"^Y" => (s::MIState,o...)->edit_yank(s),
@@ -2634,7 +2682,7 @@ end
26342682
run_interface(::Prompt) = nothing
26352683

26362684
init_state(terminal, prompt::Prompt) =
2637-
PromptState(terminal, prompt, IOBuffer(), :off, IOBuffer[], 1, InputAreaState(1, 1),
2685+
PromptState(terminal, prompt, IOBuffer(), :off, nothing, IOBuffer[], 1, InputAreaState(1, 1),
26382686
#=indent(spaces)=# -1, Threads.SpinLock(), 0.0, -Inf, nothing)
26392687

26402688
function init_state(terminal, m::ModalInterface)

stdlib/REPL/src/options.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ mutable struct Options
2727
auto_indent_time_threshold::Float64
2828
# refresh after time delay
2929
auto_refresh_time_delay::Float64
30+
hint_tab_completes::Bool
3031
# default IOContext settings at the REPL
3132
iocontext::Dict{Symbol,Any}
3233
end
@@ -47,6 +48,7 @@ Options(;
4748
auto_indent_bracketed_paste = false,
4849
auto_indent_time_threshold = 0.005,
4950
auto_refresh_time_delay = Sys.iswindows() ? 0.05 : 0.0,
51+
hint_tab_completes = true,
5052
iocontext = Dict{Symbol,Any}()) =
5153
Options(hascolor, extra_keymap, tabwidth,
5254
kill_ring_max, region_animation_duration,
@@ -55,6 +57,7 @@ Options(;
5557
backspace_align, backspace_adjust, confirm_exit,
5658
auto_indent, auto_indent_tmp_off, auto_indent_bracketed_paste,
5759
auto_indent_time_threshold, auto_refresh_time_delay,
60+
hint_tab_completes,
5861
iocontext)
5962

6063
# for use by REPLs not having an options field

stdlib/REPL/test/repl.jl

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1670,3 +1670,49 @@ fake_repl() do stdin_write, stdout_read, repl
16701670
wait(repltask)
16711671
@test contains(txt, "Some type information was truncated. Use `show(err)` to see complete types.")
16721672
end
1673+
1674+
# Hints for tab completes
1675+
1676+
fake_repl() do stdin_write, stdout_read, repl
1677+
repltask = @async begin
1678+
REPL.run_repl(repl)
1679+
end
1680+
write(stdin_write, "reada")
1681+
s1 = readuntil(stdout_read, "reada") # typed
1682+
s2 = readuntil(stdout_read, "vailable") # partial hint
1683+
1684+
write(stdin_write, "x") # "readax" doesn't tab complete so no hint
1685+
# we can't use readuntil given this doesn't print, so just wait for the hint state to be reset
1686+
while LineEdit.state(repl.mistate).hint !== nothing
1687+
sleep(0.1)
1688+
end
1689+
@test LineEdit.state(repl.mistate).hint === nothing
1690+
1691+
write(stdin_write, "\b") # only tab complete while typing forward
1692+
while LineEdit.state(repl.mistate).hint !== nothing
1693+
sleep(0.1)
1694+
end
1695+
@test LineEdit.state(repl.mistate).hint === nothing
1696+
1697+
write(stdin_write, "v")
1698+
s3 = readuntil(stdout_read, "ailable") # partial hint
1699+
1700+
write(stdin_write, "\t")
1701+
s4 = readuntil(stdout_read, "readavailable") # full completion is reprinted
1702+
1703+
write(stdin_write, "\x15\x04")
1704+
Base.wait(repltask)
1705+
end
1706+
## hints disabled
1707+
fake_repl(options=REPL.Options(confirm_exit=false,hascolor=true,hint_tab_completes=false)) do stdin_write, stdout_read, repl
1708+
repltask = @async begin
1709+
REPL.run_repl(repl)
1710+
end
1711+
write(stdin_write, "reada")
1712+
s1 = readuntil(stdout_read, "reada") # typed
1713+
@test LineEdit.state(repl.mistate).hint === nothing
1714+
1715+
write(stdin_write, "\x15\x04")
1716+
Base.wait(repltask)
1717+
@test !occursin("vailable", String(readavailable(stdout_read)))
1718+
end

0 commit comments

Comments
 (0)