From f1d61f7a970914c8039959f419df1d46e8dcb7c4 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Thu, 7 Sep 2023 13:29:19 +0100 Subject: [PATCH 1/2] add inline repl tab complete hints --- NEWS.md | 3 ++ stdlib/REPL/docs/src/index.md | 8 ++++- stdlib/REPL/src/LineEdit.jl | 58 ++++++++++++++++++++++++++++++++--- stdlib/REPL/src/options.jl | 3 ++ stdlib/REPL/test/repl.jl | 46 +++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 6 deletions(-) diff --git a/NEWS.md b/NEWS.md index 16afb8c168443..938bf9a84ae3b 100644 --- a/NEWS.md +++ b/NEWS.md @@ -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 diff --git a/stdlib/REPL/docs/src/index.md b/stdlib/REPL/docs/src/index.md index ce594d55863bc..67b64c6ca5168 100644 --- a/stdlib/REPL/docs/src/index.md +++ b/stdlib/REPL/docs/src/index.md @@ -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 @@ -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 diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index ff67e849fcc5a..6ba91e27ea8a3 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -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 @@ -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) @@ -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 @@ -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...) @@ -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), @@ -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) diff --git a/stdlib/REPL/src/options.jl b/stdlib/REPL/src/options.jl index 3ce0ab6ff00dc..1fb2c654c7df2 100644 --- a/stdlib/REPL/src/options.jl +++ b/stdlib/REPL/src/options.jl @@ -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 @@ -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, @@ -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 diff --git a/stdlib/REPL/test/repl.jl b/stdlib/REPL/test/repl.jl index 20f07864a275b..6bf65279a6b82 100644 --- a/stdlib/REPL/test/repl.jl +++ b/stdlib/REPL/test/repl.jl @@ -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 From 7b88c58c78f0abc2e63ac32817a405ad69bb5fd8 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Tue, 12 Sep 2023 21:18:44 -0400 Subject: [PATCH 2/2] fix for OhMyREPL --- stdlib/REPL/src/LineEdit.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index 6ba91e27ea8a3..ae928cb8e5082 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -375,14 +375,14 @@ end function check_for_hint(s::MIState) st = state(s) - options(st).hint_tab_completes || return nothing + 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 refresh_line(s) + return true end elseif length(completions) > 1 p = common_prefix(completions) @@ -390,16 +390,16 @@ function check_for_hint(s::MIState) hint = p[sizeof(partial)+1:end] if !isempty(hint) st.hint = hint - return refresh_line(s) + 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 refresh_line(s) + return true else - return nothing + return false end end @@ -2473,7 +2473,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)), + "*" => (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),