From 1221160bc43926efcf0fd7c56a2ab4f0fd062086 Mon Sep 17 00:00:00 2001 From: Ian Butterworth Date: Mon, 6 May 2024 13:03:10 -0400 Subject: [PATCH] REPL: fix hinting without expanding user (#54311) Fixes https://github.com/JuliaLang/julia/issues/53884 Hints will show without expanding `~`, then a tab will complete the shown hint, then a second tab on the resulting valid path expands `~`. I think it makes sense? https://github.com/JuliaLang/julia/assets/1694067/05a4fa97-2a85-4f90-8591-162256cf0704 --- stdlib/REPL/src/LineEdit.jl | 14 ++--- stdlib/REPL/src/REPL.jl | 12 ++--- stdlib/REPL/src/REPLCompletions.jl | 86 +++++++++++++++++++++++------- 3 files changed, 81 insertions(+), 31 deletions(-) diff --git a/stdlib/REPL/src/LineEdit.jl b/stdlib/REPL/src/LineEdit.jl index a910bfaebab06..1003dc0e51d84 100644 --- a/stdlib/REPL/src/LineEdit.jl +++ b/stdlib/REPL/src/LineEdit.jl @@ -179,11 +179,11 @@ struct EmptyHistoryProvider <: HistoryProvider end reset_state(::EmptyHistoryProvider) = nothing -complete_line(c::EmptyCompletionProvider, s) = String[], "", true +complete_line(c::EmptyCompletionProvider, s; hint::Bool=false) = String[], "", true # complete_line can be specialized for only two arguments, when the active module # doesn't matter (e.g. Pkg does this) -complete_line(c::CompletionProvider, s, ::Module) = complete_line(c, s) +complete_line(c::CompletionProvider, s, ::Module; hint::Bool=false) = complete_line(c, s; hint) terminal(s::IO) = s terminal(s::PromptState) = s.terminal @@ -380,7 +380,7 @@ function check_for_hint(s::MIState) # Requires making space for them earlier in refresh_multi_line return clear_hint(st) end - completions, partial, should_complete = complete_line(st.p.complete, st, s.active_module)::Tuple{Vector{String},String,Bool} + completions, partial, should_complete = complete_line(st.p.complete, st, s.active_module; hint = true)::Tuple{Vector{String},String,Bool} 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 @@ -416,8 +416,8 @@ function clear_hint(s::ModeState) 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} +function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=false) + completions, partial, should_complete = complete_line(s.p.complete, s, mod; hint)::Tuple{Vector{String},String,Bool} isempty(completions) && return false if !should_complete # should_complete is false for cases where we only want to show @@ -2149,8 +2149,8 @@ setmodifiers!(p::Prompt, m::Modifiers) = setmodifiers!(p.complete, m) setmodifiers!(c) = nothing # Search Mode completions -function complete_line(s::SearchState, repeats, mod::Module) - completions, partial, should_complete = complete_line(s.histprompt.complete, s, mod) +function complete_line(s::SearchState, repeats, mod::Module; hint::Bool=false) + completions, partial, should_complete = complete_line(s.histprompt.complete, s, mod; hint) # For now only allow exact completions in search mode if length(completions) == 1 prev_pos = position(s) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index f75e91da67be2..083c33e099c01 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -649,26 +649,26 @@ end beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1]) -function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module) +function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false) partial = beforecursor(s.input_buffer) full = LineEdit.input_string(s) - ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift) + ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint) c.modifiers = LineEdit.Modifiers() return unique!(map(completion_text, ret)), partial[range], should_complete end -function complete_line(c::ShellCompletionProvider, s::PromptState) +function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false) # First parse everything up to the current position partial = beforecursor(s.input_buffer) full = LineEdit.input_string(s) - ret, range, should_complete = shell_completions(full, lastindex(partial)) + ret, range, should_complete = shell_completions(full, lastindex(partial), hint) return unique!(map(completion_text, ret)), partial[range], should_complete end -function complete_line(c::LatexCompletions, s) +function complete_line(c::LatexCompletions, s; hint::Bool=false) partial = beforecursor(LineEdit.buffer(s)) full = LineEdit.input_string(s)::String - ret, range, should_complete = bslash_completions(full, lastindex(partial))[2] + ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2] return unique!(map(completion_text, ret)), partial[range], should_complete end diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index 5ffa718f38604..0d2846dcadc90 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -367,7 +367,8 @@ function complete_path(path::AbstractString; use_envpath=false, shell_escape=false, raw_escape=false, - string_escape=false) + string_escape=false, + contract_user=false) @assert !(shell_escape && string_escape) if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path) # if the path is just "~", don't consider the expanded username as a prefix @@ -413,7 +414,7 @@ function complete_path(path::AbstractString; matches = ((shell_escape ? do_shell_escape(s) : string_escape ? do_string_escape(s) : s) for s in matches) matches = ((raw_escape ? do_raw_escape(s) : s) for s in matches) - matches = Completion[PathCompletion(s) for s in matches] + matches = Completion[PathCompletion(contract_user ? contractuser(s) : s) for s in matches] return matches, dir, !isempty(matches) end @@ -421,7 +422,8 @@ function complete_path(path::AbstractString, pos::Int; use_envpath=false, shell_escape=false, - string_escape=false) + string_escape=false, + contract_user=false) ## TODO: enable this depwarn once Pkg is fixed #Base.depwarn("complete_path with pos argument is deprecated because the return value [2] is incorrect to use", :complete_path) paths, dir, success = complete_path(path; use_envpath, shell_escape, string_escape) @@ -909,7 +911,7 @@ function close_path_completion(dir, paths, str, pos) return lastindex(str) <= pos || str[nextind(str, pos)] != '"' end -function bslash_completions(string::String, pos::Int) +function bslash_completions(string::String, pos::Int, hint::Bool=false) slashpos = something(findprev(isequal('\\'), string, pos), 0) if (something(findprev(in(bslash_separators), string, pos), 0) < slashpos && !(1 < slashpos && (string[prevind(string, slashpos)]=='\\'))) @@ -1166,7 +1168,7 @@ function complete_identifiers!(suggestions::Vector{Completion}, @nospecialize(ff return sort!(unique(suggestions), by=completion_text), (dotpos+1):pos, true end -function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true) +function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true, hint::Bool=false) # First parse everything up to the current position partial = string[1:pos] inc_tag = Base.incomplete_tag(Meta.parse(partial, raise=false, depwarn=false)) @@ -1219,6 +1221,9 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif # its invocation. varrange = findprev("var\"", string, pos) + expanded = nothing + was_expanded = false + if varrange !== nothing ok, ret = bslash_completions(string, pos) ok && return ret @@ -1235,7 +1240,13 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif scs::String = string[r] expanded = complete_expanduser(scs, r) - expanded[3] && return expanded # If user expansion available, return it + was_expanded = expanded[3] + if was_expanded + scs = (only(expanded[1])::PathCompletion).path + # If tab press, ispath and user expansion available, return it now + # otherwise see if we can complete the path further before returning with expanded ~ + !hint && ispath(scs) && return expanded::Completions + end path::String = replace(scs, r"(\\+)\g1(\\?)`" => "\1\2`") # fuzzy unescape_raw_string: match an even number of \ before ` and replace with half as many # This expansion with "\\ "=>' ' replacement and shell_escape=true @@ -1253,12 +1264,19 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif r = nextind(string, startpos + sizeof(dir)):pos else map!(paths, paths) do c::PathCompletion - return PathCompletion(dir * "/" * c.path) + p = dir * "/" * c.path + was_expanded && (p = contractuser(p)) + return PathCompletion(p) end end end end - return sort!(paths, by=p->p.path), r, success + if isempty(paths) && !hint && was_expanded + # if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion + return expanded::Completions + else + return sort!(paths, by=p->p.path), r::UnitRange{Int}, success + end end elseif inc_tag === :string # Find first non-escaped quote @@ -1268,7 +1286,13 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif scs::String = string[r] expanded = complete_expanduser(scs, r) - expanded[3] && return expanded # If user expansion available, return it + was_expanded = expanded[3] + if was_expanded + scs = (only(expanded[1])::PathCompletion).path + # If tab press, ispath and user expansion available, return it now + # otherwise see if we can complete the path further before returning with expanded ~ + !hint && ispath(scs) && return expanded::Completions + end path = try unescape_string(replace(scs, "\\\$"=>"\$")) @@ -1280,7 +1304,9 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif paths, dir, success = complete_path(path::String, string_escape=true) if close_path_completion(dir, paths, path, pos) - paths[1] = PathCompletion((paths[1]::PathCompletion).path * "\"") + p = (paths[1]::PathCompletion).path * "\"" + hint && was_expanded && (p = contractuser(p)) + paths[1] = PathCompletion(p) end if success && !isempty(dir) @@ -1289,21 +1315,31 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif # otherwise make it the whole completion if endswith(dir, "/") && startswith(scs, dir) r = (startpos + sizeof(dir)):pos - elseif startswith(scs, dir * "/") + elseif startswith(scs, dir * "/") && dir != dirname(homedir()) + was_expanded && (dir = contractuser(dir)) r = nextind(string, startpos + sizeof(dir)):pos else map!(paths, paths) do c::PathCompletion - return PathCompletion(dir * "/" * c.path) + p = dir * "/" * c.path + hint && was_expanded && (p = contractuser(p)) + return PathCompletion(p) end end end end # Fallthrough allowed so that Latex symbols can be completed in strings - success && return sort!(paths, by=p->p.path), r, success + if success + return sort!(paths, by=p->p.path), r::UnitRange{Int}, success + elseif !hint && was_expanded + # if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion + return expanded::Completions + end end end end + # if path has ~ and we didn't find any paths to complete just return the expanded path + was_expanded && return expanded::Completions ok, ret = bslash_completions(string, pos) ok && return ret @@ -1389,7 +1425,7 @@ end module_filter(mod::Module, x::Symbol) = Base.isbindingresolved(mod, x) && isdefined(mod, x) && isa(getglobal(mod, x), Module) -function shell_completions(string, pos) +function shell_completions(string, pos, hint::Bool=false) # First parse everything up to the current position scs = string[1:pos] args, last_arg_start = try @@ -1407,7 +1443,7 @@ function shell_completions(string, pos) # If the last char was a space, but shell_parse ignored it search on "". if isexpr(lastarg, :incomplete) || isexpr(lastarg, :error) partial = string[last_arg_start:pos] - ret, range = completions(partial, lastindex(partial)) + ret, range = completions(partial, lastindex(partial), Main, true, hint) range = range .+ (last_arg_start - 1) return ret, range, true elseif endswith(scs, ' ') && !endswith(scs, "\\ ") @@ -1422,9 +1458,16 @@ function shell_completions(string, pos) # Also try looking into the env path if the user wants to complete the first argument use_envpath = length(args.args) < 2 - # TODO: call complete_expanduser here? + expanded = complete_expanduser(path, r) + was_expanded = expanded[3] + if was_expanded + path = (only(expanded[1])::PathCompletion).path + # If tab press, ispath and user expansion available, return it now + # otherwise see if we can complete the path further before returning with expanded ~ + !hint && ispath(path) && return expanded::Completions + end - paths, dir, success = complete_path(path, use_envpath=use_envpath, shell_escape=true) + paths, dir, success = complete_path(path, use_envpath=use_envpath, shell_escape=true, contract_user=was_expanded) if success && !isempty(dir) let dir = do_shell_escape(dir) @@ -1442,7 +1485,14 @@ function shell_completions(string, pos) end end end - + # if ~ was expanded earlier and the incomplete string isn't a path + # return the path with contracted user to match what the hint shows. Otherwise expand ~ + # i.e. require two tab presses to expand user + if was_expanded && !ispath(path) + map!(paths, paths) do c::PathCompletion + PathCompletion(contractuser(c.path)) + end + end return paths, r, success end return Completion[], 0:-1, false