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

REPL: fix hinting without expanding user #54311

Merged
merged 1 commit into from
May 6, 2024
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
14 changes: 7 additions & 7 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
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 @@ -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

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