Skip to content

Commit

Permalink
Add REPL-completion for keyword arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
Liozou committed Jan 30, 2022
1 parent e319a39 commit 5ed2042
Show file tree
Hide file tree
Showing 2 changed files with 188 additions and 0 deletions.
82 changes: 82 additions & 0 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ struct DictCompletion <: Completion
key::String
end

struct KeywordArgumentCompletion <: Completion
kwarg::String
end

# interface definition
function Base.getproperty(c::Completion, name::Symbol)
if name === :text
Expand All @@ -85,6 +89,8 @@ function Base.getproperty(c::Completion, name::Symbol)
return getfield(c, :text)::String
elseif name === :key
return getfield(c, :key)::String
elseif name === :kwarg
return getfield(c, :kwarg)::String
end
return getfield(c, name)
end
Expand All @@ -100,6 +106,7 @@ _completion_text(c::MethodCompletion) = repr(c.method)
_completion_text(c::BslashCompletion) = c.bslash
_completion_text(c::ShellCompletion) = c.text
_completion_text(c::DictCompletion) = c.key
_completion_text(c::KeywordArgumentCompletion) = c.kwarg*'='

completion_text(c) = _completion_text(c)::String

Expand Down Expand Up @@ -748,6 +755,76 @@ end
return matches
end

# provide completion for keyword arguments in function calls
function complete_keyword_argument(partial, last_idx, context_module)
fail = Completion[], 0:-1

# Quickly abandon if the situation does not look like the completion of a kwarg
idx_last_punct = something(findprev(x -> ispunct(x) && x != '_' && x != '!', partial, last_idx), 0)::Int
idx_last_punct == 0 && return fail
last_punct = partial[idx_last_punct]
last_punct == ',' || last_punct == ';' || last_punct == '(' || return fail
before_last_word_start = something(findprev(in(non_identifier_chars), partial, last_idx), 0)
before_last_word_start == 0 && return fail
all(isspace, partial[nextind(partial, idx_last_punct):before_last_word_start]) || return fail
frange, method_name_end = find_start_brace(partial[1:idx_last_punct])
method_name_end frange || return fail

# At this point, we are guaranteed to be in a set of parentheses, possibly a function
# call, and the last word (currently being completed) has no internal dot (i.e. of the
# form "foo.bar") and is directly preceded by `last_punct` (one of ',', ';' or '(').
# Now, check that we are indeed in a function call
frange = first(frange):(last_punct==';' ? prevind(partial, idx_last_punct) : idx_last_punct)
s = replace(partial[frange], r"\!+([^=\(]+)" => s"\1") # strip preceding ! operator
ex = Meta.parse(s * ')', raise=false, depwarn=false)
isa(ex, Expr) || return fail
ex.head === :call || (ex.head === :. && ex.args[2] isa Expr && (ex.args[2]::Expr).head === :tuple) || return fail

# inlined `complete_methods` function since we need the `kwargs_ex` variable
func, found = get_value(ex.args[1], context_module)
!(found::Bool) && return fail
args_ex, kwargs_ex = complete_methods_args(ex.args[2:end], ex, context_module, true, true)
used_kwargs = Set{Symbol}(kwargs_ex)

# Only try to complete as a kwarg if the context makes it clear that the current
# argument could be a kwarg (i.e. right after ';' or if there is another kwarg)
isempty(used_kwargs) && last_punct != ';' &&
all(x -> !(x isa Expr) || (x.head !== :kw && x.head !== :parameters), ex.args[2:end]) &&
return fail

methods = Completion[]
complete_methods!(methods, Core.Typeof(func), args_ex, kwargs_ex, -1)

# Finally, for each method corresponding to the function call, provide completions
# suggestions for each keyword that starts like the last word and that is not already
# used previously in the expression. The corresponding suggestion is "kwname="
# If the keyword corresponds to an existing name, also include "kwname" as a suggestion
# since the syntax `foo(; bar)` is equivalent to `foo(; bar=bar)`
wordrange = nextind(partial, before_last_word_start):last_idx
last_word = partial[wordrange] # the word to complete
kwargs = Set{String}()
for m in methods
m::MethodCompletion
possible_kwargs = Base.kwarg_decl(m.method)
current_kwarg_candidates = String[]
for _kw in possible_kwargs
kw = String(_kw)
if !endswith(kw, "...") && startswith(kw, last_word) && _kw used_kwargs
push!(current_kwarg_candidates, kw)
end
end
union!(kwargs, current_kwarg_candidates)
end

suggestions = Completion[]
for kwarg in kwargs
push!(suggestions, KeywordArgumentCompletion(kwarg))
end
append!(suggestions, complete_symbol(last_word, (mod,x)->true, context_module))

return sort!(suggestions, by=completion_text), wordrange
end

function project_deps_get_completion_candidates(pkgstarts::String, project_file::String)
loading_candidates = String[]
d = Base.parsed_toml(project_file)
Expand Down Expand Up @@ -848,6 +925,11 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif
return Completion[], 0:-1, false
end

# Check whether we can complete a keyword argument in a function call
kwarg_completion, wordrange = complete_keyword_argument(partial, pos, context_module)
isempty(wordrange) || return kwarg_completion, wordrange, !isempty(kwarg_completion)


dotpos = something(findprev(isequal('.'), string, pos), 0)
startpos = nextind(string, something(findprev(in(non_identifier_chars), string, pos), 0))
# strip preceding ! operator
Expand Down
106 changes: 106 additions & 0 deletions stdlib/REPL/test/replcompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ let ex = quote

kwtest(; x=1, y=2, w...) = pass
kwtest2(a; x=1, y=2, w...) = pass
kwtest3(a::Number; length, len2, foobar, kwargs...) = pass
kwtest3(a::Real; another!kwarg, len2) = pass
kwtest3(a::Integer; namedarg, foobar, slurp...) = pass
kwtest4(a::AbstractString; _a1b, x23) = pass
kwtest4(a::String; _a1b, xαβγ) = pass
kwtest4(a::SubString; x23, _something) = pass

const named = (; len2=3)

array = [1, 1]
varfloat = 0.1
Expand Down Expand Up @@ -787,6 +795,13 @@ let s = "CompletionFoo.test6()[1](CompletionFoo.Test_y(rand())).y"
@test c[1] == "yy"
end

let s = "CompletionFoo.named."
c, r = test_complete(s)
@test length(c) == 1
@test r == (lastindex(s) + 1):lastindex(s)
@test c[1] == "len2"
end

# Test completion in multi-line comments
let s = "#=\n\\alpha"
c, r, res = test_complete(s)
Expand Down Expand Up @@ -1258,6 +1273,97 @@ test_dict_completion("test_repl_comp_customdict")
@test "tϵsτcmδ`" in c
end

@testset "Keyword-argument completion" begin
c, r = test_complete("CompletionFoo.kwtest3(a;foob")
@test c == ["foobar="]
c, r = test_complete("CompletionFoo.kwtest3(a; le")
@test "length" c # provide this kind of completion in case the user wants to splat a variable
@test "length=" c
@test "len2=" c
@test "len2" c
c, r = test_complete("CompletionFoo.kwtest3.(a;\nlength")
@test "length" c
@test "length=" c
c, r = test_complete("CompletionFoo.kwtest3(a, length=4, l")
@test "length" c
@test "length=" c # since it was already used, do not suggest it again
@test "len2=" c
c, r = test_complete("CompletionFoo.kwtest3(a; kwargs..., fo")
@test "foreach" c # provide this kind of completion in case the user wants to splat a variable
@test "foobar=" c
c, r = test_complete("CompletionFoo.kwtest3(a; another!kwarg=0, le")
@test "length" c
@test "length=" c # the first method could be called and `anotherkwarg` slurped
@test "len2=" c
c, r = test_complete("CompletionFoo.kwtest3(a; another!")
@test c == ["another!kwarg="]
c, r = test_complete("CompletionFoo.kwtest3(a; another!kwarg=0, foob")
@test c == ["foobar="] # the first method could be called and `anotherkwarg` slurped
c, r = test_complete("CompletionFoo.kwtest3(a; namedarg=0, foob")
@test c == ["foobar="]

# Check for confusion with CompletionFoo.named
c, r = test_complete_foo("kwtest3(blabla; unknown=4, namedar")
@test c == ["namedarg="]
c, r = test_complete_foo("kwtest3(blabla; named")
@test "named" c
@test "namedarg=" c
@test "len2" c
c, r = test_complete_foo("kwtest3(blabla; named.")
@test c == ["len2"]
c, r = test_complete_foo("kwtest3(blabla; named..., another!")
@test c == ["another!kwarg="]
c, r = test_complete_foo("kwtest3(blabla; named..., len")
@test "length" c
@test "length=" c
@test "len2=" c
c, r = test_complete_foo("kwtest3(1+3im; named")
@test "named" c
@test "namedarg=" c
@test "len2" c
c, r = test_complete_foo("kwtest3(1+3im; named.")
@test c == ["len2"]

c, r = test_complete("CompletionFoo.kwtest4(a; x23=0, _")
@test "_a1b=" c
@test "_something=" c
c, r = test_complete("CompletionFoo.kwtest4(a; xαβγ=1, _")
@test "_a1b=" c
@test "_something=" c # no such keyword for the method with keyword `xαβγ`
c, r = test_complete("CompletionFoo.kwtest4(a; x23=0, x")
@test "x23=" c
@test "xαβγ=" c
c, r = test_complete("CompletionFoo.kwtest4(a; _a1b=1, x")
@test "x23=" c
@test "xαβγ=" c


# return true if no completion suggests a keyword argument
function hasnokwsuggestions(str)
c, _ = test_complete(str)
return !any(x -> endswith(x, r"[a-z]="), c)
end
@test hasnokwsuggestions("Completio")
@test hasnokwsuggestions("CompletionFoo.kwt")
@test hasnokwsuggestions("CompletionFoo.kwtest3(")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a")
@test hasnokwsuggestions("CompletionFoo.kwtest3(le")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a;")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; len2=")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; len2=le")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; len2=3 ")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; [le")
@test hasnokwsuggestions("CompletionFoo.kwtest3([length; le")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; (le")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; foo(le")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; (; le")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; length, ")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; kwargs..., ")
@test hasnokwsuggestions("CompletionFoo.kwtest3(a; unknown=4, another!kw") # only methods 1 and 3 could slurp `unknown`
@test hasnokwsuggestions("CompletionFoo.kwtest3(1+3im; nameda")
@test hasnokwsuggestions("CompletionFoo.kwtest3(12//7; foob") # because of specificity
end

# Test completion in context

# No CompletionFoo.CompletionFoo
Expand Down

0 comments on commit 5ed2042

Please sign in to comment.