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

REPLCompletions: Add completions for var"" identifiers #49294

Merged
merged 2 commits into from
Apr 10, 2023
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 79 additions & 47 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,8 @@ function completes_global(x, name)
end

function appendmacro!(syms, macros, needle, endchar)
for s in macros
for macsym in macros
s = String(macsym)
if endswith(s, needle)
from = nextind(s, firstindex(s))
to = prevind(s, sizeof(s)-sizeof(needle)+1)
Expand All @@ -131,28 +132,21 @@ end
function filtered_mod_names(ffunc::Function, mod::Module, name::AbstractString, all::Bool = false, imported::Bool = false)
ssyms = names(mod, all = all, imported = imported)
filter!(ffunc, ssyms)
syms = String[string(s) for s in ssyms]
macros = filter(x -> startswith(x, "@" * name), syms)
macros = filter(x -> startswith(String(x), "@" * name), ssyms)
syms = String[sprint((io,s)->Base.show_sym(io, s; allow_macroname=true), s) for s in ssyms if completes_global(String(s), name)]
appendmacro!(syms, macros, "_str", "\"")
appendmacro!(syms, macros, "_cmd", "`")
filter!(x->completes_global(x, name), syms)
return [ModuleCompletion(mod, sym) for sym in syms]
end

# REPL Symbol Completions
function complete_symbol(sym::String, @nospecialize(ffunc), context_module::Module=Main)
function complete_symbol(@nospecialize(ex), name::String, @nospecialize(ffunc), context_module::Module=Main)
mod = context_module
name = sym

lookup_module = true
t = Union{}
val = nothing
if something(findlast(in(non_identifier_chars), sym), 0) < something(findlast(isequal('.'), sym), 0)
# Find module
lookup_name, name = rsplit(sym, ".", limit=2)

ex = Meta.parse(lookup_name, raise=false, depwarn=false)

if ex !== nothing
res = repl_eval_ex(ex, context_module)
res === nothing && return Completion[]
if res isa Const
Expand Down Expand Up @@ -898,7 +892,7 @@ function complete_keyword_argument(partial, last_idx, context_module)
end

suggestions = Completion[KeywordArgumentCompletion(kwarg) for kwarg in kwargs]
append!(suggestions, complete_symbol(last_word, Returns(true), context_module))
append!(suggestions, complete_symbol(nothing, last_word, Returns(true), context_module))

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

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)
ex = nothing
comp_keywords && append!(suggestions, complete_keyword(name))
if dotpos > 1 && string[dotpos] == '.'
s = string[1:dotpos-1]
# First see if the whole string up to `pos` is a valid expression. If so, use it.
ex = Meta.parse(s, raise=false, depwarn=false)
if isexpr(ex, :incomplete)
s = string[startpos:pos]
# Heuristic to find the start of the expression. TODO: This would be better
# done with a proper error-recovering parser.
if 0 < startpos <= lastindex(string) && string[startpos] == '.'
i = prevind(string, startpos)
while 0 < i
c = string[i]
if c in (')', ']')
if c == ')'
c_start = '('
c_end = ')'
elseif c == ']'
c_start = '['
c_end = ']'
end
frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
isempty(frange) && break # unbalanced parens
startpos = first(frange)
i = prevind(string, startpos)
elseif c in ('\'', '\"', '\`')
s = "$c$c"*string[startpos:pos]
break
else
break
end
s = string[startpos:pos]
end
end
if something(findlast(in(non_identifier_chars), s), 0) < something(findlast(isequal('.'), s), 0)
lookup_name, name = rsplit(s, ".", limit=2)
name = String(name)

ex = Meta.parse(lookup_name, raise=false, depwarn=false)
end
isexpr(ex, :incomplete) && (ex = nothing)
end
end
append!(suggestions, complete_symbol(ex, name, ffunc, context_module))
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)
# First parse everything up to the current position
partial = string[1:pos]
Expand Down Expand Up @@ -962,8 +1005,25 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
length(matches)>0 && return Completion[DictCompletion(identifier, match) for match in sort!(matches)], loc::Int:pos, true
end

ffunc = Returns(true)
suggestions = Completion[]

# Check if this is a var"" string macro that should be completed like
# an identifier rather than a string.
# TODO: It would be nice for the parser to give us more information here
# so that we can lookup the macro by identity rather than pattern matching
# its invocation.
varrange = findprev("var\"", string, pos)

if varrange !== nothing
ok, ret = bslash_completions(string, pos)
ok && return ret
startpos = first(varrange) + 4
dotpos = something(findprev(isequal('.'), string, startpos), 0)
return complete_identifiers!(Completion[], ffunc, context_module, string,
string[startpos:pos], pos, dotpos, startpos)
# otherwise...
if inc_tag in [:cmd, :string]
elseif inc_tag in [:cmd, :string]
m = match(r"[\t\n\r\"`><=*?|]| (?!\\)", reverse(partial))
startpos = nextind(partial, reverseind(partial, m.offset))
r = startpos:pos
Expand Down Expand Up @@ -1010,9 +1070,8 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
startpos += length(m.match)
end

ffunc = Returns(true)
suggestions = Completion[]
comp_keywords = true
name = string[max(startpos, dotpos+1):pos]
comp_keywords = !isempty(name) && startpos > dotpos
if afterusing(string, startpos)
# We're right after using or import. Let's look only for packages
# and modules we can reach from here
Expand Down Expand Up @@ -1054,38 +1113,11 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
ffunc = (mod,x)->(Base.isbindingresolved(mod, x) && isdefined(mod, x) && isa(getfield(mod, x), Module))
comp_keywords = false
end

startpos == 0 && (pos = -1)
dotpos < startpos && (dotpos = startpos - 1)
s = string[startpos:pos]
comp_keywords && append!(suggestions, complete_keyword(s))
# if the start of the string is a `.`, try to consume more input to get back to the beginning of the last expression
if 0 < startpos <= lastindex(string) && string[startpos] == '.'
i = prevind(string, startpos)
while 0 < i
c = string[i]
if c in (')', ']')
if c == ')'
c_start = '('
c_end = ')'
elseif c == ']'
c_start = '['
c_end = ']'
end
frange, end_of_identifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
isempty(frange) && break # unbalanced parens
startpos = first(frange)
i = prevind(string, startpos)
elseif c in ('\'', '\"', '\`')
s = "$c$c"*string[startpos:pos]
break
else
break
end
s = string[startpos:pos]
end
end
append!(suggestions, complete_symbol(s, ffunc, context_module))
return sort!(unique(suggestions), by=completion_text), (dotpos+1):pos, true
return complete_identifiers!(suggestions, ffunc, context_module, string,
name, pos, dotpos, startpos, comp_keywords)
end

function shell_completions(string, pos)
Expand Down
29 changes: 29 additions & 0 deletions stdlib/REPL/test/replcompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ let ex = quote
macro testcmd_cmd(s) end
macro tϵsτcmδ_cmd(s) end

var"complicated symbol with spaces" = 5

struct WeirdNames end
Base.propertynames(::WeirdNames) = (Symbol("oh no!"), Symbol("oh yes!"))

end # module CompletionFoo
test_repl_comp_dict = CompletionFoo.test_dict
test_repl_comp_customdict = CompletionFoo.test_customdict
Expand Down Expand Up @@ -1801,3 +1806,27 @@ let s = "pop!(global_xs)."
@test "value" in c
end
@test length(global_xs) == 1 # the completion above shouldn't evaluate `pop!` call

# Test completion of var"" identifiers (#49280)
let s = "var\"complicated "
c, r = test_complete_foo(s)
@test c == Any["var\"complicated symbol with spaces\""]
end

let s = "WeirdNames().var\"oh "
c, r = test_complete_foo(s)
@test c == Any["var\"oh no!\"", "var\"oh yes!\""]
end

# Test completion of non-Expr literals
let s = "\"abc\"."
c, r = test_complete(s)
# (no completion, but shouldn't error)
@test isempty(c)
end

let s = "`abc`.e"
c, r = test_complete(s)
# (completions for the fields of `Cmd`)
@test c == Any["env", "exec"]
end