Skip to content

Commit

Permalink
REPL: fix hinting without expanding user (#54311)
Browse files Browse the repository at this point in the history
Fixes #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
(cherry picked from commit 1221160)
  • Loading branch information
IanButterworth authored and KristofferC committed May 23, 2024
1 parent c0735fe commit d4e097b
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 31 deletions.
14 changes: 7 additions & 7 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -180,11 +180,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
Expand Down Expand Up @@ -381,7 +381,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
Expand Down Expand Up @@ -417,8 +417,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
Expand Down Expand Up @@ -2150,8 +2150,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)
Expand Down
12 changes: 6 additions & 6 deletions stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -610,26 +610,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

Expand Down
86 changes: 68 additions & 18 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -413,15 +414,16 @@ 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

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)
Expand Down Expand Up @@ -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)]=='\\')))
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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, "\\\$"=>"\$"))
Expand All @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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, "\\ ")
Expand All @@ -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)
Expand All @@ -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
Expand Down

0 comments on commit d4e097b

Please sign in to comment.