Skip to content

Commit d0f4327

Browse files
Kenoaviatesk
andauthored
REPLCompletions: Add completions for var"" identifiers (#49294)
* REPLCompletions: Add completions for var"" identifiers Fixes #49280. Mostly just moving code around, but there's one extra place where we're pattern matching var"". I do hope that after the future parser replacement, we can do these things on the in-progress AST rather than textually. * Apply suggestions from code review --------- Co-authored-by: Shuhei Kadowaki <[email protected]>
1 parent 94da492 commit d0f4327

File tree

2 files changed

+108
-47
lines changed

2 files changed

+108
-47
lines changed

stdlib/REPL/src/REPLCompletions.jl

Lines changed: 79 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ function completes_global(x, name)
119119
end
120120

121121
function appendmacro!(syms, macros, needle, endchar)
122-
for s in macros
122+
for macsym in macros
123+
s = String(macsym)
123124
if endswith(s, needle)
124125
from = nextind(s, firstindex(s))
125126
to = prevind(s, sizeof(s)-sizeof(needle)+1)
@@ -131,28 +132,21 @@ end
131132
function filtered_mod_names(ffunc::Function, mod::Module, name::AbstractString, all::Bool = false, imported::Bool = false)
132133
ssyms = names(mod, all = all, imported = imported)
133134
filter!(ffunc, ssyms)
134-
syms = String[string(s) for s in ssyms]
135-
macros = filter(x -> startswith(x, "@" * name), syms)
135+
macros = filter(x -> startswith(String(x), "@" * name), ssyms)
136+
syms = String[sprint((io,s)->Base.show_sym(io, s; allow_macroname=true), s) for s in ssyms if completes_global(String(s), name)]
136137
appendmacro!(syms, macros, "_str", "\"")
137138
appendmacro!(syms, macros, "_cmd", "`")
138-
filter!(x->completes_global(x, name), syms)
139139
return [ModuleCompletion(mod, sym) for sym in syms]
140140
end
141141

142142
# REPL Symbol Completions
143-
function complete_symbol(sym::String, @nospecialize(ffunc), context_module::Module=Main)
143+
function complete_symbol(@nospecialize(ex), name::String, @nospecialize(ffunc), context_module::Module=Main)
144144
mod = context_module
145-
name = sym
146145

147146
lookup_module = true
148147
t = Union{}
149148
val = nothing
150-
if something(findlast(in(non_identifier_chars), sym), 0) < something(findlast(isequal('.'), sym), 0)
151-
# Find module
152-
lookup_name, name = rsplit(sym, ".", limit=2)
153-
154-
ex = Meta.parse(lookup_name, raise=false, depwarn=false)
155-
149+
if ex !== nothing
156150
res = repl_eval_ex(ex, context_module)
157151
res === nothing && return Completion[]
158152
if res isa Const
@@ -898,7 +892,7 @@ function complete_keyword_argument(partial, last_idx, context_module)
898892
end
899893

900894
suggestions = Completion[KeywordArgumentCompletion(kwarg) for kwarg in kwargs]
901-
append!(suggestions, complete_symbol(last_word, Returns(true), context_module))
895+
append!(suggestions, complete_symbol(nothing, last_word, Returns(true), context_module))
902896

903897
return sort!(suggestions, by=completion_text), wordrange
904898
end
@@ -919,6 +913,55 @@ function project_deps_get_completion_candidates(pkgstarts::String, project_file:
919913
return Completion[PackageCompletion(name) for name in loading_candidates]
920914
end
921915

916+
function complete_identifiers!(suggestions::Vector{Completion}, @nospecialize(ffunc::Function), context_module::Module, string::String, name::String, pos::Int, dotpos::Int, startpos::Int, comp_keywords=false)
917+
ex = nothing
918+
comp_keywords && append!(suggestions, complete_keyword(name))
919+
if dotpos > 1 && string[dotpos] == '.'
920+
s = string[1:dotpos-1]
921+
# First see if the whole string up to `pos` is a valid expression. If so, use it.
922+
ex = Meta.parse(s, raise=false, depwarn=false)
923+
if isexpr(ex, :incomplete)
924+
s = string[startpos:pos]
925+
# Heuristic to find the start of the expression. TODO: This would be better
926+
# done with a proper error-recovering parser.
927+
if 0 < startpos <= lastindex(string) && string[startpos] == '.'
928+
i = prevind(string, startpos)
929+
while 0 < i
930+
c = string[i]
931+
if c in (')', ']')
932+
if c == ')'
933+
c_start = '('
934+
c_end = ')'
935+
elseif c == ']'
936+
c_start = '['
937+
c_end = ']'
938+
end
939+
frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
940+
isempty(frange) && break # unbalanced parens
941+
startpos = first(frange)
942+
i = prevind(string, startpos)
943+
elseif c in ('\'', '\"', '\`')
944+
s = "$c$c"*string[startpos:pos]
945+
break
946+
else
947+
break
948+
end
949+
s = string[startpos:pos]
950+
end
951+
end
952+
if something(findlast(in(non_identifier_chars), s), 0) < something(findlast(isequal('.'), s), 0)
953+
lookup_name, name = rsplit(s, ".", limit=2)
954+
name = String(name)
955+
956+
ex = Meta.parse(lookup_name, raise=false, depwarn=false)
957+
end
958+
isexpr(ex, :incomplete) && (ex = nothing)
959+
end
960+
end
961+
append!(suggestions, complete_symbol(ex, name, ffunc, context_module))
962+
return sort!(unique(suggestions), by=completion_text), (dotpos+1):pos, true
963+
end
964+
922965
function completions(string::String, pos::Int, context_module::Module=Main, shift::Bool=true)
923966
# First parse everything up to the current position
924967
partial = string[1:pos]
@@ -962,8 +1005,25 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
9621005
length(matches)>0 && return Completion[DictCompletion(identifier, match) for match in sort!(matches)], loc::Int:pos, true
9631006
end
9641007

1008+
ffunc = Returns(true)
1009+
suggestions = Completion[]
1010+
1011+
# Check if this is a var"" string macro that should be completed like
1012+
# an identifier rather than a string.
1013+
# TODO: It would be nice for the parser to give us more information here
1014+
# so that we can lookup the macro by identity rather than pattern matching
1015+
# its invocation.
1016+
varrange = findprev("var\"", string, pos)
1017+
1018+
if varrange !== nothing
1019+
ok, ret = bslash_completions(string, pos)
1020+
ok && return ret
1021+
startpos = first(varrange) + 4
1022+
dotpos = something(findprev(isequal('.'), string, startpos), 0)
1023+
return complete_identifiers!(Completion[], ffunc, context_module, string,
1024+
string[startpos:pos], pos, dotpos, startpos)
9651025
# otherwise...
966-
if inc_tag in [:cmd, :string]
1026+
elseif inc_tag in [:cmd, :string]
9671027
m = match(r"[\t\n\r\"`><=*?|]| (?!\\)", reverse(partial))
9681028
startpos = nextind(partial, reverseind(partial, m.offset))
9691029
r = startpos:pos
@@ -1010,9 +1070,8 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
10101070
startpos += length(m.match)
10111071
end
10121072

1013-
ffunc = Returns(true)
1014-
suggestions = Completion[]
1015-
comp_keywords = true
1073+
name = string[max(startpos, dotpos+1):pos]
1074+
comp_keywords = !isempty(name) && startpos > dotpos
10161075
if afterusing(string, startpos)
10171076
# We're right after using or import. Let's look only for packages
10181077
# and modules we can reach from here
@@ -1054,38 +1113,11 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
10541113
ffunc = (mod,x)->(Base.isbindingresolved(mod, x) && isdefined(mod, x) && isa(getfield(mod, x), Module))
10551114
comp_keywords = false
10561115
end
1116+
10571117
startpos == 0 && (pos = -1)
10581118
dotpos < startpos && (dotpos = startpos - 1)
1059-
s = string[startpos:pos]
1060-
comp_keywords && append!(suggestions, complete_keyword(s))
1061-
# if the start of the string is a `.`, try to consume more input to get back to the beginning of the last expression
1062-
if 0 < startpos <= lastindex(string) && string[startpos] == '.'
1063-
i = prevind(string, startpos)
1064-
while 0 < i
1065-
c = string[i]
1066-
if c in (')', ']')
1067-
if c == ')'
1068-
c_start = '('
1069-
c_end = ')'
1070-
elseif c == ']'
1071-
c_start = '['
1072-
c_end = ']'
1073-
end
1074-
frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
1075-
isempty(frange) && break # unbalanced parens
1076-
startpos = first(frange)
1077-
i = prevind(string, startpos)
1078-
elseif c in ('\'', '\"', '\`')
1079-
s = "$c$c"*string[startpos:pos]
1080-
break
1081-
else
1082-
break
1083-
end
1084-
s = string[startpos:pos]
1085-
end
1086-
end
1087-
append!(suggestions, complete_symbol(s, ffunc, context_module))
1088-
return sort!(unique(suggestions), by=completion_text), (dotpos+1):pos, true
1119+
return complete_identifiers!(suggestions, ffunc, context_module, string,
1120+
name, pos, dotpos, startpos, comp_keywords)
10891121
end
10901122

10911123
function shell_completions(string, pos)

stdlib/REPL/test/replcompletions.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,11 @@ let ex = quote
132132
macro testcmd_cmd(s) end
133133
macro tϵsτcmδ_cmd(s) end
134134

135+
var"complicated symbol with spaces" = 5
136+
137+
struct WeirdNames end
138+
Base.propertynames(::WeirdNames) = (Symbol("oh no!"), Symbol("oh yes!"))
139+
135140
end # module CompletionFoo
136141
test_repl_comp_dict = CompletionFoo.test_dict
137142
test_repl_comp_customdict = CompletionFoo.test_customdict
@@ -1801,3 +1806,27 @@ let s = "pop!(global_xs)."
18011806
@test "value" in c
18021807
end
18031808
@test length(global_xs) == 1 # the completion above shouldn't evaluate `pop!` call
1809+
1810+
# Test completion of var"" identifiers (#49280)
1811+
let s = "var\"complicated "
1812+
c, r = test_complete_foo(s)
1813+
@test c == Any["var\"complicated symbol with spaces\""]
1814+
end
1815+
1816+
let s = "WeirdNames().var\"oh "
1817+
c, r = test_complete_foo(s)
1818+
@test c == Any["var\"oh no!\"", "var\"oh yes!\""]
1819+
end
1820+
1821+
# Test completion of non-Expr literals
1822+
let s = "\"abc\"."
1823+
c, r = test_complete(s)
1824+
# (no completion, but shouldn't error)
1825+
@test isempty(c)
1826+
end
1827+
1828+
let s = "`abc`.e"
1829+
c, r = test_complete(s)
1830+
# (completions for the fields of `Cmd`)
1831+
@test c == Any["env", "exec"]
1832+
end

0 commit comments

Comments
 (0)