Skip to content

Commit d4e097b

Browse files
IanButterworthKristofferC
authored and
KristofferC
committed
REPL: fix hinting without expanding user (#54311)
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)
1 parent c0735fe commit d4e097b

File tree

3 files changed

+81
-31
lines changed

3 files changed

+81
-31
lines changed

stdlib/REPL/src/LineEdit.jl

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -180,11 +180,11 @@ struct EmptyHistoryProvider <: HistoryProvider end
180180

181181
reset_state(::EmptyHistoryProvider) = nothing
182182

183-
complete_line(c::EmptyCompletionProvider, s) = String[], "", true
183+
complete_line(c::EmptyCompletionProvider, s; hint::Bool=false) = String[], "", true
184184

185185
# complete_line can be specialized for only two arguments, when the active module
186186
# doesn't matter (e.g. Pkg does this)
187-
complete_line(c::CompletionProvider, s, ::Module) = complete_line(c, s)
187+
complete_line(c::CompletionProvider, s, ::Module; hint::Bool=false) = complete_line(c, s; hint)
188188

189189
terminal(s::IO) = s
190190
terminal(s::PromptState) = s.terminal
@@ -381,7 +381,7 @@ function check_for_hint(s::MIState)
381381
# Requires making space for them earlier in refresh_multi_line
382382
return clear_hint(st)
383383
end
384-
completions, partial, should_complete = complete_line(st.p.complete, st, s.active_module)::Tuple{Vector{String},String,Bool}
384+
completions, partial, should_complete = complete_line(st.p.complete, st, s.active_module; hint = true)::Tuple{Vector{String},String,Bool}
385385
isempty(completions) && return clear_hint(st)
386386
# Don't complete for single chars, given e.g. `x` completes to `xor`
387387
if length(partial) > 1 && should_complete
@@ -417,8 +417,8 @@ function clear_hint(s::ModeState)
417417
end
418418
end
419419

420-
function complete_line(s::PromptState, repeats::Int, mod::Module)
421-
completions, partial, should_complete = complete_line(s.p.complete, s, mod)::Tuple{Vector{String},String,Bool}
420+
function complete_line(s::PromptState, repeats::Int, mod::Module; hint::Bool=false)
421+
completions, partial, should_complete = complete_line(s.p.complete, s, mod; hint)::Tuple{Vector{String},String,Bool}
422422
isempty(completions) && return false
423423
if !should_complete
424424
# should_complete is false for cases where we only want to show
@@ -2150,8 +2150,8 @@ setmodifiers!(p::Prompt, m::Modifiers) = setmodifiers!(p.complete, m)
21502150
setmodifiers!(c) = nothing
21512151

21522152
# Search Mode completions
2153-
function complete_line(s::SearchState, repeats, mod::Module)
2154-
completions, partial, should_complete = complete_line(s.histprompt.complete, s, mod)
2153+
function complete_line(s::SearchState, repeats, mod::Module; hint::Bool=false)
2154+
completions, partial, should_complete = complete_line(s.histprompt.complete, s, mod; hint)
21552155
# For now only allow exact completions in search mode
21562156
if length(completions) == 1
21572157
prev_pos = position(s)

stdlib/REPL/src/REPL.jl

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -610,26 +610,26 @@ end
610610

611611
beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])
612612

613-
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module)
613+
function complete_line(c::REPLCompletionProvider, s::PromptState, mod::Module; hint::Bool=false)
614614
partial = beforecursor(s.input_buffer)
615615
full = LineEdit.input_string(s)
616-
ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift)
616+
ret, range, should_complete = completions(full, lastindex(partial), mod, c.modifiers.shift, hint)
617617
c.modifiers = LineEdit.Modifiers()
618618
return unique!(map(completion_text, ret)), partial[range], should_complete
619619
end
620620

621-
function complete_line(c::ShellCompletionProvider, s::PromptState)
621+
function complete_line(c::ShellCompletionProvider, s::PromptState; hint::Bool=false)
622622
# First parse everything up to the current position
623623
partial = beforecursor(s.input_buffer)
624624
full = LineEdit.input_string(s)
625-
ret, range, should_complete = shell_completions(full, lastindex(partial))
625+
ret, range, should_complete = shell_completions(full, lastindex(partial), hint)
626626
return unique!(map(completion_text, ret)), partial[range], should_complete
627627
end
628628

629-
function complete_line(c::LatexCompletions, s)
629+
function complete_line(c::LatexCompletions, s; hint::Bool=false)
630630
partial = beforecursor(LineEdit.buffer(s))
631631
full = LineEdit.input_string(s)::String
632-
ret, range, should_complete = bslash_completions(full, lastindex(partial))[2]
632+
ret, range, should_complete = bslash_completions(full, lastindex(partial), hint)[2]
633633
return unique!(map(completion_text, ret)), partial[range], should_complete
634634
end
635635

stdlib/REPL/src/REPLCompletions.jl

Lines changed: 68 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -367,7 +367,8 @@ function complete_path(path::AbstractString;
367367
use_envpath=false,
368368
shell_escape=false,
369369
raw_escape=false,
370-
string_escape=false)
370+
string_escape=false,
371+
contract_user=false)
371372
@assert !(shell_escape && string_escape)
372373
if Base.Sys.isunix() && occursin(r"^~(?:/|$)", path)
373374
# if the path is just "~", don't consider the expanded username as a prefix
@@ -413,15 +414,16 @@ function complete_path(path::AbstractString;
413414

414415
matches = ((shell_escape ? do_shell_escape(s) : string_escape ? do_string_escape(s) : s) for s in matches)
415416
matches = ((raw_escape ? do_raw_escape(s) : s) for s in matches)
416-
matches = Completion[PathCompletion(s) for s in matches]
417+
matches = Completion[PathCompletion(contract_user ? contractuser(s) : s) for s in matches]
417418
return matches, dir, !isempty(matches)
418419
end
419420

420421
function complete_path(path::AbstractString,
421422
pos::Int;
422423
use_envpath=false,
423424
shell_escape=false,
424-
string_escape=false)
425+
string_escape=false,
426+
contract_user=false)
425427
## TODO: enable this depwarn once Pkg is fixed
426428
#Base.depwarn("complete_path with pos argument is deprecated because the return value [2] is incorrect to use", :complete_path)
427429
paths, dir, success = complete_path(path; use_envpath, shell_escape, string_escape)
@@ -909,7 +911,7 @@ function close_path_completion(dir, paths, str, pos)
909911
return lastindex(str) <= pos || str[nextind(str, pos)] != '"'
910912
end
911913

912-
function bslash_completions(string::String, pos::Int)
914+
function bslash_completions(string::String, pos::Int, hint::Bool=false)
913915
slashpos = something(findprev(isequal('\\'), string, pos), 0)
914916
if (something(findprev(in(bslash_separators), string, pos), 0) < slashpos &&
915917
!(1 < slashpos && (string[prevind(string, slashpos)]=='\\')))
@@ -1166,7 +1168,7 @@ function complete_identifiers!(suggestions::Vector{Completion}, @nospecialize(ff
11661168
return sort!(unique(suggestions), by=completion_text), (dotpos+1):pos, true
11671169
end
11681170

1169-
function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true)
1171+
function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true, hint::Bool=false)
11701172
# First parse everything up to the current position
11711173
partial = string[1:pos]
11721174
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
12191221
# its invocation.
12201222
varrange = findprev("var\"", string, pos)
12211223

1224+
expanded = nothing
1225+
was_expanded = false
1226+
12221227
if varrange !== nothing
12231228
ok, ret = bslash_completions(string, pos)
12241229
ok && return ret
@@ -1235,7 +1240,13 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
12351240
scs::String = string[r]
12361241

12371242
expanded = complete_expanduser(scs, r)
1238-
expanded[3] && return expanded # If user expansion available, return it
1243+
was_expanded = expanded[3]
1244+
if was_expanded
1245+
scs = (only(expanded[1])::PathCompletion).path
1246+
# If tab press, ispath and user expansion available, return it now
1247+
# otherwise see if we can complete the path further before returning with expanded ~
1248+
!hint && ispath(scs) && return expanded::Completions
1249+
end
12391250

12401251
path::String = replace(scs, r"(\\+)\g1(\\?)`" => "\1\2`") # fuzzy unescape_raw_string: match an even number of \ before ` and replace with half as many
12411252
# This expansion with "\\ "=>' ' replacement and shell_escape=true
@@ -1253,12 +1264,19 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
12531264
r = nextind(string, startpos + sizeof(dir)):pos
12541265
else
12551266
map!(paths, paths) do c::PathCompletion
1256-
return PathCompletion(dir * "/" * c.path)
1267+
p = dir * "/" * c.path
1268+
was_expanded && (p = contractuser(p))
1269+
return PathCompletion(p)
12571270
end
12581271
end
12591272
end
12601273
end
1261-
return sort!(paths, by=p->p.path), r, success
1274+
if isempty(paths) && !hint && was_expanded
1275+
# if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion
1276+
return expanded::Completions
1277+
else
1278+
return sort!(paths, by=p->p.path), r::UnitRange{Int}, success
1279+
end
12621280
end
12631281
elseif inc_tag === :string
12641282
# Find first non-escaped quote
@@ -1268,7 +1286,13 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
12681286
scs::String = string[r]
12691287

12701288
expanded = complete_expanduser(scs, r)
1271-
expanded[3] && return expanded # If user expansion available, return it
1289+
was_expanded = expanded[3]
1290+
if was_expanded
1291+
scs = (only(expanded[1])::PathCompletion).path
1292+
# If tab press, ispath and user expansion available, return it now
1293+
# otherwise see if we can complete the path further before returning with expanded ~
1294+
!hint && ispath(scs) && return expanded::Completions
1295+
end
12721296

12731297
path = try
12741298
unescape_string(replace(scs, "\\\$"=>"\$"))
@@ -1280,7 +1304,9 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
12801304
paths, dir, success = complete_path(path::String, string_escape=true)
12811305

12821306
if close_path_completion(dir, paths, path, pos)
1283-
paths[1] = PathCompletion((paths[1]::PathCompletion).path * "\"")
1307+
p = (paths[1]::PathCompletion).path * "\""
1308+
hint && was_expanded && (p = contractuser(p))
1309+
paths[1] = PathCompletion(p)
12841310
end
12851311

12861312
if success && !isempty(dir)
@@ -1289,21 +1315,31 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
12891315
# otherwise make it the whole completion
12901316
if endswith(dir, "/") && startswith(scs, dir)
12911317
r = (startpos + sizeof(dir)):pos
1292-
elseif startswith(scs, dir * "/")
1318+
elseif startswith(scs, dir * "/") && dir != dirname(homedir())
1319+
was_expanded && (dir = contractuser(dir))
12931320
r = nextind(string, startpos + sizeof(dir)):pos
12941321
else
12951322
map!(paths, paths) do c::PathCompletion
1296-
return PathCompletion(dir * "/" * c.path)
1323+
p = dir * "/" * c.path
1324+
hint && was_expanded && (p = contractuser(p))
1325+
return PathCompletion(p)
12971326
end
12981327
end
12991328
end
13001329
end
13011330

13021331
# Fallthrough allowed so that Latex symbols can be completed in strings
1303-
success && return sort!(paths, by=p->p.path), r, success
1332+
if success
1333+
return sort!(paths, by=p->p.path), r::UnitRange{Int}, success
1334+
elseif !hint && was_expanded
1335+
# if not able to provide completions, not hinting, and ~ expansion was possible, return ~ expansion
1336+
return expanded::Completions
1337+
end
13041338
end
13051339
end
13061340
end
1341+
# if path has ~ and we didn't find any paths to complete just return the expanded path
1342+
was_expanded && return expanded::Completions
13071343

13081344
ok, ret = bslash_completions(string, pos)
13091345
ok && return ret
@@ -1389,7 +1425,7 @@ end
13891425
module_filter(mod::Module, x::Symbol) =
13901426
Base.isbindingresolved(mod, x) && isdefined(mod, x) && isa(getglobal(mod, x), Module)
13911427

1392-
function shell_completions(string, pos)
1428+
function shell_completions(string, pos, hint::Bool=false)
13931429
# First parse everything up to the current position
13941430
scs = string[1:pos]
13951431
args, last_arg_start = try
@@ -1407,7 +1443,7 @@ function shell_completions(string, pos)
14071443
# If the last char was a space, but shell_parse ignored it search on "".
14081444
if isexpr(lastarg, :incomplete) || isexpr(lastarg, :error)
14091445
partial = string[last_arg_start:pos]
1410-
ret, range = completions(partial, lastindex(partial))
1446+
ret, range = completions(partial, lastindex(partial), Main, true, hint)
14111447
range = range .+ (last_arg_start - 1)
14121448
return ret, range, true
14131449
elseif endswith(scs, ' ') && !endswith(scs, "\\ ")
@@ -1422,9 +1458,16 @@ function shell_completions(string, pos)
14221458
# Also try looking into the env path if the user wants to complete the first argument
14231459
use_envpath = length(args.args) < 2
14241460

1425-
# TODO: call complete_expanduser here?
1461+
expanded = complete_expanduser(path, r)
1462+
was_expanded = expanded[3]
1463+
if was_expanded
1464+
path = (only(expanded[1])::PathCompletion).path
1465+
# If tab press, ispath and user expansion available, return it now
1466+
# otherwise see if we can complete the path further before returning with expanded ~
1467+
!hint && ispath(path) && return expanded::Completions
1468+
end
14261469

1427-
paths, dir, success = complete_path(path, use_envpath=use_envpath, shell_escape=true)
1470+
paths, dir, success = complete_path(path, use_envpath=use_envpath, shell_escape=true, contract_user=was_expanded)
14281471

14291472
if success && !isempty(dir)
14301473
let dir = do_shell_escape(dir)
@@ -1442,7 +1485,14 @@ function shell_completions(string, pos)
14421485
end
14431486
end
14441487
end
1445-
1488+
# if ~ was expanded earlier and the incomplete string isn't a path
1489+
# return the path with contracted user to match what the hint shows. Otherwise expand ~
1490+
# i.e. require two tab presses to expand user
1491+
if was_expanded && !ispath(path)
1492+
map!(paths, paths) do c::PathCompletion
1493+
PathCompletion(contractuser(c.path))
1494+
end
1495+
end
14461496
return paths, r, success
14471497
end
14481498
return Completion[], 0:-1, false

0 commit comments

Comments
 (0)