Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add repl tab complete hints while typing #51229

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 false
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 true
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 true
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 true
else
return false
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
Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am experiencing frequent REPL corruption now where I lose part of the line or it prints wrong. For example, if you type

julia> using Cthu, REPL

then mouse back and change that to

julia> using Cthulhu, REPL

when you start typing, it will garble the rest of the line. Then when you type the final u, the line gets fully deleted. Once you type another character, then it gets restored correctly.

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a fix ready. Basically it limits to only doing hints for the end of the line. Inserting hints at earlier positions would take a lot of work in the printing logic I think so best to just limit it for now.

Maybe it's possible to implement earlier hints before 1.11 is out.

Also I'm considering not hinting on single characters, given x shows xor as a hint which is a bit awkward.

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That seems useful. Could you also make the hint print with light colors?

Copy link
Sponsor Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It already prints in :light_black which seems like the right choice?

Copy link
Sponsor Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see, I am using TERM=screen-256color and I don't have the terminal file for that (only for screen-256color-s with an added status line feature), so Julia is not printing any colors for me

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) && refresh_line(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
46 changes: 46 additions & 0 deletions stdlib/REPL/test/repl.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1670,3 +1670,49 @@ 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

# Hints for tab completes

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
## hints disabled
fake_repl(options=REPL.Options(confirm_exit=false,hascolor=true,hint_tab_completes=false)) do stdin_write, stdout_read, repl
repltask = @async begin
REPL.run_repl(repl)
end
write(stdin_write, "reada")
s1 = readuntil(stdout_read, "reada") # typed
@test LineEdit.state(repl.mistate).hint === nothing

write(stdin_write, "\x15\x04")
Base.wait(repltask)
@test !occursin("vailable", String(readavailable(stdout_read)))
end