Skip to content

Commit

Permalink
add inline repl tab complete hints
Browse files Browse the repository at this point in the history
  • Loading branch information
IanButterworth committed Sep 12, 2023
1 parent 5d82d80 commit c07ef6b
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 6 deletions.
3 changes: 3 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ Standard library changes

#### REPL

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

#### SuiteSparse


Expand Down
8 changes: 7 additions & 1 deletion stdlib/REPL/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ Users should refer to `LineEdit.jl` to discover the available actions on key inp

## Tab completion

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

```julia-repl
Expand All @@ -334,6 +334,12 @@ julia> mapfold[TAB]
mapfoldl mapfoldr
```

When a single complete tab-complete result is available a hint of the completion will show in a lighter color.
This can be disabled via `Base.active_repl.options.hint_tab_completes = false`.

!!! compat "Julia 1.11"
Tab-complete hinting was added in Julia 1.11

Like other components of the REPL, the search is case-sensitive:

```julia-repl
Expand Down
58 changes: 53 additions & 5 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ mutable struct PromptState <: ModeState
p::Prompt
input_buffer::IOBuffer
region_active::Symbol # :shift or :mark or :off
hint::Union{String,Nothing}
undo_buffers::Vector{IOBuffer}
undo_idx::Int
ias::InputAreaState
Expand Down Expand Up @@ -361,7 +362,7 @@ function show_completions(s::PromptState, completions::Vector{String})
end
end

# Prompt Completions
# Prompt Completions & Hints
function complete_line(s::MIState)
set_action!(s, :complete_line)
if complete_line(state(s), s.key_repeats, s.active_module)
Expand All @@ -372,6 +373,36 @@ function complete_line(s::MIState)
end
end

function check_for_hint(s::MIState)
st = state(s)
options(st).hint_tab_completes || return nothing
completions, partial, should_complete = complete_line(st.p.complete, st, s.active_module)::Tuple{Vector{String},String,Bool}
if should_complete
if length(completions) == 1
hint = only(completions)[sizeof(partial)+1:end]
if !isempty(hint) # completion on a complete name returns itself so check that there's something to hint
st.hint = hint
return refresh_line(s)
end
elseif length(completions) > 1
p = common_prefix(completions)
if p in completions # i.e. complete `@time` even though `@time_imports` etc. exists
hint = p[sizeof(partial)+1:end]
if !isempty(hint)
st.hint = hint
return refresh_line(s)
end
end
end
end
if !isnothing(st.hint)
st.hint = "" # don't set to nothing here. That will be done in `maybe_show_hint`
return refresh_line(s)
else
return nothing
end
end

function complete_line(s::PromptState, repeats::Int, mod::Module)
completions, partial, should_complete = complete_line(s.p.complete, s, mod)::Tuple{Vector{String},String,Bool}
isempty(completions) && return false
Expand Down Expand Up @@ -432,12 +463,29 @@ prompt_string(p::Prompt) = prompt_string(p.prompt)
prompt_string(s::AbstractString) = s
prompt_string(f::Function) = Base.invokelatest(f)

function maybe_show_hint(s::PromptState)
isa(s.hint, String) || return nothing
# The hint being "" then nothing is used to first clear a previous hint, then skip printing the hint
# the clear line cannot be printed each time because it breaks column movement
if isempty(s.hint)
print(terminal(s), "\e[0K") # clear remainder of line which had a hint
s.hint = nothing
else
Base.printstyled(terminal(s), s.hint, color=:light_black)
cmove_left(terminal(s), textwidth(s.hint))
s.hint = "" # being "" signals to do one clear line remainder to clear the hint next time if still empty
end
return nothing
end

function refresh_multi_line(s::PromptState; kw...)
if s.refresh_wait !== nothing
close(s.refresh_wait)
s.refresh_wait = nothing
end
refresh_multi_line(terminal(s), s; kw...)
r = refresh_multi_line(terminal(s), s; kw...)
maybe_show_hint(s)
return r
end
refresh_multi_line(s::ModeState; kw...) = refresh_multi_line(terminal(s), s; kw...)
refresh_multi_line(termbuf::TerminalBuffer, s::ModeState; kw...) = refresh_multi_line(termbuf, terminal(s), s; kw...)
Expand Down Expand Up @@ -2424,8 +2472,8 @@ AnyDict(
"\e\n" => "\e\r",
"^_" => (s::MIState,o...)->edit_undo!(s),
"\e_" => (s::MIState,o...)->edit_redo!(s),
# Simply insert it into the buffer by default
"*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c)),
# Show hints at what tab complete would do by default
"*" => (s::MIState,data,c::StringLike)->(edit_insert(s, c); check_for_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 @@ -2634,7 +2682,7 @@ end
run_interface(::Prompt) = nothing

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

function init_state(terminal, m::ModalInterface)
Expand Down
3 changes: 3 additions & 0 deletions stdlib/REPL/src/options.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ mutable struct Options
auto_indent_time_threshold::Float64
# refresh after time delay
auto_refresh_time_delay::Float64
hint_tab_completes::Bool
# default IOContext settings at the REPL
iocontext::Dict{Symbol,Any}
end
Expand All @@ -47,6 +48,7 @@ Options(;
auto_indent_bracketed_paste = false,
auto_indent_time_threshold = 0.005,
auto_refresh_time_delay = Sys.iswindows() ? 0.05 : 0.0,
hint_tab_completes = true,
iocontext = Dict{Symbol,Any}()) =
Options(hascolor, extra_keymap, tabwidth,
kill_ring_max, region_animation_duration,
Expand All @@ -55,6 +57,7 @@ Options(;
backspace_align, backspace_adjust, confirm_exit,
auto_indent, auto_indent_tmp_off, auto_indent_bracketed_paste,
auto_indent_time_threshold, auto_refresh_time_delay,
hint_tab_completes,
iocontext)

# for use by REPLs not having an options field
Expand Down
31 changes: 31 additions & 0 deletions stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1670,3 +1670,34 @@ fake_repl() do stdin_write, stdout_read, repl
wait(repltask)
@test contains(txt, "Some type information was truncated. Use `show(err)` to see complete types.")
end

fake_repl() do stdin_write, stdout_read, repl
repltask = @async begin
REPL.run_repl(repl)
end
write(stdin_write, "reada")
s1 = readuntil(stdout_read, "reada") # typed
s2 = readuntil(stdout_read, "vailable") # partial hint

write(stdin_write, "x") # "readax" doesn't tab complete so no hint
# we can't use readuntil given this doesn't print, so just wait for the hint state to be reset
while LineEdit.state(repl.mistate).hint !== nothing
sleep(0.1)
end
@test LineEdit.state(repl.mistate).hint === nothing

write(stdin_write, "\b") # only tab complete while typing forward
while LineEdit.state(repl.mistate).hint !== nothing
sleep(0.1)
end
@test LineEdit.state(repl.mistate).hint === nothing

write(stdin_write, "v")
s3 = readuntil(stdout_read, "ailable") # partial hint

write(stdin_write, "\t")
s4 = readuntil(stdout_read, "readavailable") # full completion is reprinted

write(stdin_write, "\x15\x04")
Base.wait(repltask)
end

0 comments on commit c07ef6b

Please sign in to comment.