diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index fe25efc1795cc0..5de150a109c7d9 100644 --- a/stdlib/REPL/src/REPLCompletions.jl +++ b/stdlib/REPL/src/REPLCompletions.jl @@ -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 @@ -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 @@ -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 @@ -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) @@ -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 diff --git a/stdlib/REPL/test/replcompletions.jl b/stdlib/REPL/test/replcompletions.jl index 3f30f0ca88feba..794694f13bba82 100644 --- a/stdlib/REPL/test/replcompletions.jl +++ b/stdlib/REPL/test/replcompletions.jl @@ -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 @@ -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) @@ -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