From c5a63d8b6465c97ffef282b81d5a42627fe1468b Mon Sep 17 00:00:00 2001 From: Lionel Zoubritzky Date: Fri, 22 Jul 2022 22:45:03 +0200 Subject: [PATCH] Add REPL-completions for keyword arguments (#43536) --- stdlib/REPL/src/REPLCompletions.jl | 208 ++++++++++++++------ stdlib/REPL/test/replcompletions.jl | 294 ++++++++++++++++++++++++++-- 2 files changed, 429 insertions(+), 73 deletions(-) diff --git a/stdlib/REPL/src/REPLCompletions.jl b/stdlib/REPL/src/REPLCompletions.jl index a06700fbcc591..3c16c804ccebc 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 @@ -316,22 +323,6 @@ function complete_expanduser(path::AbstractString, r) return Completion[PathCompletion(expanded)], r, path != expanded end -# Determines whether method_complete should be tried. It should only be done if -# the string endswiths ',' or '(' when disregarding whitespace_chars -function should_method_complete(s::AbstractString) - method_complete = false - for c in reverse(s) - if c in [',', '(', ';'] - method_complete = true - break - elseif !(c in whitespace_chars) - method_complete = false - break - end - end - method_complete -end - # Returns a range that includes the method name in front of the first non # closed start brace from the end of the string. function find_start_brace(s::AbstractString; c_start='(', c_end=')') @@ -530,50 +521,59 @@ end # Method completion on function call expression that look like :(max(1)) MAX_METHOD_COMPLETIONS::Int = 40 -function complete_methods(ex_org::Expr, context_module::Module=Main, shift::Bool=false) - out = Completion[] +function _complete_methods(ex_org::Expr, context_module::Module, shift::Bool) funct, found = get_type(ex_org.args[1], context_module)::Tuple{Any,Bool} - !found && return out + !found && return 2, funct, [], Set{Symbol}() - args_ex, kwargs_ex = complete_methods_args(ex_org.args[2:end], ex_org, context_module, true, true) - push!(args_ex, Vararg{Any}) - complete_methods!(out, funct, args_ex, kwargs_ex, shift ? -2 : MAX_METHOD_COMPLETIONS) + args_ex, kwargs_ex, kwargs_flag = complete_methods_args(ex_org, context_module, true, true) + return kwargs_flag, funct, args_ex, kwargs_ex +end +function complete_methods(ex_org::Expr, context_module::Module=Main, shift::Bool=false) + kwargs_flag, funct, args_ex, kwargs_ex = _complete_methods(ex_org, context_module, shift)::Tuple{Int, Any, Vector{Any}, Set{Symbol}} + out = Completion[] + kwargs_flag == 2 && return out # one of the kwargs is invalid + kwargs_flag == 0 && push!(args_ex, Vararg{Any}) # allow more arguments if there is no semicolon + complete_methods!(out, funct, args_ex, kwargs_ex, shift ? -2 : MAX_METHOD_COMPLETIONS, kwargs_flag == 1) return out end MAX_ANY_METHOD_COMPLETIONS::Int = 10 function complete_any_methods(ex_org::Expr, callee_module::Module, context_module::Module, moreargs::Bool, shift::Bool) out = Completion[] - args_ex, kwargs_ex = try + args_ex, kwargs_ex, kwargs_flag = try # this may throw, since we set default_any to false - complete_methods_args(ex_org.args[2:end], ex_org, context_module, false, false) + complete_methods_args(ex_org, context_module, false, false) catch ex ex isa ArgumentError || rethrow() return out end + kwargs_flag == 2 && return out # one of the kwargs is invalid + + # moreargs determines whether to accept more args, independently of the presence of a + # semicolon for the ".?(" syntax moreargs && push!(args_ex, Vararg{Any}) seen = Base.IdSet() for name in names(callee_module; all=true) - if !Base.isdeprecated(callee_module, name) && isdefined(callee_module, name) + if !Base.isdeprecated(callee_module, name) && isdefined(callee_module, name) && !startswith(string(name), '#') func = getfield(callee_module, name) if !isa(func, Module) funct = Core.Typeof(func) if !in(funct, seen) push!(seen, funct) - complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS) + complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS, false) end elseif callee_module === Main && isa(func, Module) callee_module2 = func for name in names(callee_module2) - if !Base.isdeprecated(callee_module2, name) && isdefined(callee_module2, name) + if !Base.isdeprecated(callee_module2, name) && isdefined(callee_module2, name) && !startswith(string(name), '#') func = getfield(callee_module, name) if !isa(func, Module) funct = Core.Typeof(func) if !in(funct, seen) push!(seen, funct) - complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS) + complete_methods!(out, funct, args_ex, kwargs_ex, MAX_ANY_METHOD_COMPLETIONS, false) end end end @@ -595,32 +595,57 @@ function complete_any_methods(ex_org::Expr, callee_module::Module, context_modul return out end -function complete_methods_args(funargs::Vector{Any}, ex_org::Expr, context_module::Module, default_any::Bool, allow_broadcasting::Bool) +function detect_invalid_kwarg!(kwargs_ex::Vector{Symbol}, @nospecialize(x), kwargs_flag::Int, possible_splat::Bool) + n = isexpr(x, :kw) ? x.args[1] : x + if n isa Symbol + push!(kwargs_ex, n) + return kwargs_flag + end + possible_splat && isexpr(x, :...) && return kwargs_flag + return 2 # The kwarg is invalid +end + +function detect_args_kwargs(funargs::Vector{Any}, context_module::Module, default_any::Bool, broadcasting::Bool) args_ex = Any[] - kwargs_ex = false - if allow_broadcasting && ex_org.head === :. && ex_org.args[2] isa Expr - # handle broadcasting, but only handle number of arguments instead of - # argument types - for _ in (ex_org.args[2]::Expr).args - push!(args_ex, Any) - end - else - for ex in funargs - if isexpr(ex, :parameters) - if !isempty(ex.args) - kwargs_ex = true - end - elseif isexpr(ex, :kw) - kwargs_ex = true + kwargs_ex = Symbol[] + kwargs_flag = 0 + # kwargs_flag is: + # * 0 if there is no semicolon and no invalid kwarg + # * 1 if there is a semicolon and no invalid kwarg + # * 2 if there are two semicolons or more, or if some kwarg is invalid, which + # means that it is not of the form "bar=foo", "bar" or "bar..." + for i in (1+!broadcasting):length(funargs) + ex = funargs[i] + if isexpr(ex, :parameters) + kwargs_flag = ifelse(kwargs_flag == 0, 1, 2) # there should be at most one :parameters + for x in ex.args + kwargs_flag = detect_invalid_kwarg!(kwargs_ex, x, kwargs_flag, true) + end + elseif isexpr(ex, :kw) + kwargs_flag = detect_invalid_kwarg!(kwargs_ex, ex, kwargs_flag, false) + else + if broadcasting + # handle broadcasting, but only handle number of arguments instead of + # argument types + push!(args_ex, Any) else push!(args_ex, get_type(get_type(ex, context_module)..., default_any)) end end end - return args_ex, kwargs_ex + return args_ex, Set{Symbol}(kwargs_ex), kwargs_flag end -function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_ex::Vector{Any}, kwargs_ex::Bool, max_method_completions::Int) +is_broadcasting_expr(ex::Expr) = ex.head === :. && isexpr(ex.args[2], :tuple) + +function complete_methods_args(ex::Expr, context_module::Module, default_any::Bool, allow_broadcasting::Bool) + if allow_broadcasting && is_broadcasting_expr(ex) + return detect_args_kwargs((ex.args[2]::Expr).args, context_module, default_any, true) + end + return detect_args_kwargs(ex.args, context_module, default_any, false) +end + +function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_ex::Vector{Any}, kwargs_ex::Set{Symbol}, max_method_completions::Int, exact_nargs::Bool) # Input types and number of arguments t_in = Tuple{funct, args_ex...} m = Base._methods_by_ftype(t_in, nothing, max_method_completions, Base.get_world_counter(), @@ -633,6 +658,7 @@ function complete_methods!(out::Vector{Completion}, @nospecialize(funct), args_e # TODO: if kwargs_ex, filter out methods without kwargs? push!(out, MethodCompletion(match.spec_types, match.method)) end + # TODO: filter out methods with wrong number of arguments if `exact_nargs` is set end include("latex_symbols.jl") @@ -744,6 +770,76 @@ end return matches end +# Identify an argument being completed in a method call. If the argument is empty, method +# suggestions will be provided instead of argument completions. +function identify_possible_method_completion(partial, last_idx) + fail = 0:-1, Expr(:nothing), 0:-1, 0 + + # First, check that the last punctuation is either ',', ';' or '(' + 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 + + # Then, check that `last_punct` is only followed by an identifier or nothing + before_last_word_start = something(findprev(in(non_identifier_chars), partial, last_idx), 0) + before_last_word_start == 0 && return fail + all(isspace, @view partial[nextind(partial, idx_last_punct):before_last_word_start]) || return fail + + # Check that `last_punct` is either the last '(' or placed after a previous '(' + frange, method_name_end = find_start_brace(@view partial[1:idx_last_punct]) + method_name_end ∈ frange || return fail + + # Strip the preceding ! operators, if any, and close the expression with a ')' + s = replace(partial[frange], r"\G\!+([^=\(]+)" => s"\1"; count=1) * ')' + ex = Meta.parse(s, raise=false, depwarn=false) + isa(ex, Expr) || return fail + + # `wordrange` is the position of the last argument to complete + wordrange = nextind(partial, before_last_word_start):last_idx + return frange, ex, wordrange, method_name_end +end + +# Provide completion for keyword arguments in function calls +function complete_keyword_argument(partial, last_idx, context_module) + frange, ex, wordrange, = identify_possible_method_completion(partial, last_idx) + fail = Completion[], 0:-1, frange + ex.head === :call || is_broadcasting_expr(ex) || return fail + + kwargs_flag, funct, args_ex, kwargs_ex = _complete_methods(ex, context_module, true)::Tuple{Int, Any, Vector{Any}, Set{Symbol}} + kwargs_flag == 2 && return fail # one of the previous kwargs is invalid + + methods = Completion[] + complete_methods!(methods, funct, Any[Vararg{Any}], kwargs_ex, -1, kwargs_flag == 1) + # TODO: use args_ex instead of Any[Vararg{Any}] and only provide kwarg completion for + # method calls compatible with the current arguments. + + # For each method corresponding to the function call, provide completion 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(; kwname)" is equivalent to "foo(; kwname=kwname)". + 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 ∉ kwargs_ex + push!(current_kwarg_candidates, kw) + end + end + union!(kwargs, current_kwarg_candidates) + end + + suggestions = Completion[KeywordArgumentCompletion(kwarg) for kwarg in kwargs] + append!(suggestions, complete_symbol(last_word, Returns(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) @@ -827,16 +923,12 @@ function completions(string::String, pos::Int, context_module::Module=Main, shif # Make sure that only bslash_completions is working on strings inc_tag === :string && return Completion[], 0:-1, false - if inc_tag === :other && should_method_complete(partial) - frange, method_name_end = find_start_brace(partial) - # strip preceding ! operator - s = replace(partial[frange], r"\!+([^=\(]+)" => s"\1") - ex = Meta.parse(s * ")", raise=false, depwarn=false) - - if isa(ex, Expr) + if inc_tag === :other + frange, ex, wordrange, method_name_end = identify_possible_method_completion(partial, pos) + if last(frange) != -1 && all(isspace, @view partial[wordrange]) # no last argument to complete if ex.head === :call return complete_methods(ex, context_module, shift), first(frange):method_name_end, false - elseif ex.head === :. && ex.args[2] isa Expr && (ex.args[2]::Expr).head === :tuple + elseif is_broadcasting_expr(ex) return complete_methods(ex, context_module, shift), first(frange):(method_name_end - 1), false end end @@ -844,14 +936,18 @@ 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 - if (m = match(r"^\!+", string[startpos:pos])) !== nothing + if (m = match(r"\G\!+", partial, startpos)) isa RegexMatch startpos += length(m.match) end - ffunc = (mod,x)->true + ffunc = Returns(true) suggestions = Completion[] comp_keywords = true if afterusing(string, startpos) diff --git a/stdlib/REPL/test/replcompletions.jl b/stdlib/REPL/test/replcompletions.jl index f584569519c22..5d6232e4b7626 100644 --- a/stdlib/REPL/test/replcompletions.jl +++ b/stdlib/REPL/test/replcompletions.jl @@ -101,8 +101,20 @@ let ex = quote test11(x::Int, y::Int, z) = pass test11(_, _, s::String) = pass + test!12() = pass + 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 + kwtest5(a::Int, b, x...; somekwarg, somekotherkwarg) = pass + kwtest5(a::Char, b; xyz) = pass + + const named = (; len2=3) array = [1, 1] varfloat = 0.1 @@ -144,6 +156,17 @@ test_complete_noshift(s) = map_completion_text(@inferred(completions(s, lastinde module M32377 end test_complete_32377(s) = map_completion_text(completions(s,lastindex(s), M32377)) +macro test_nocompletion(s) + tests = [ + :(@test c == String[]), + :(@test res === false) + ] + for t in tests + t.args[2] = __source__ # fix the LineNumberNode + end + return Expr(:let, Expr(:(=), :((c, _, res)), :(test_complete($(esc(s))))), Expr(:block, tests...)) +end + let s = "" c, r = test_complete(s) @test "CompletionFoo" in c @@ -271,16 +294,10 @@ let end # inexistent completion inside a string -let s = "Base.print(\"lol" - c, r, res = test_complete(s) - @test res == false -end +@test_nocompletion("Base.print(\"lol") # inexistent completion inside a cmd -let s = "run(`lol" - c, r, res = test_complete(s) - @test res == false -end +@test_nocompletion("run(`lol") # test latex symbol completions let s = "\\alpha" @@ -548,27 +565,58 @@ let s = "CompletionFoo.kwtest( " @test !res @test length(c) == 1 @test occursin("x, y, w...", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.kwtest(;") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(; x=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(; kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(x=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(x=1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(x=kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest(; x=kw=1, ") end -for s in ("CompletionFoo.kwtest(;", - "CompletionFoo.kwtest(; x=1, ", - "CompletionFoo.kwtest(; kw=1, ", - ) +let s = "CompletionFoo.kwtest2(1, x=1," c, r, res = test_complete(s) @test !res @test length(c) == 1 - @test occursin("x, y, w...", c[1]) + @test occursin("a; x, y, w...", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1; x=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1, x=1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1, kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1; kw=1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1, kw=1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(y=3, 1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(y=3, 1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(kw=3, 1, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(kw=3, 1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1; ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest2(1, ") +end + +let s = "CompletionFoo.kwtest4(x23=18, x; " + c, r, res = test_complete(s) + @test !res + @test length(c) == 3 # TODO: remove "kwtest4(a::String; _a1b, xαβγ)" + @test any(str->occursin("kwtest4(a::SubString", str), c) + @test any(str->occursin("kwtest4(a::AbstractString", str), c) + @test (c, r, res) == test_complete("CompletionFoo.kwtest4(x23=18, x, ") + @test (c, r, res) == test_complete("CompletionFoo.kwtest4(x23=18, ") end -for s in ("CompletionFoo.kwtest2(1; x=1,", - "CompletionFoo.kwtest2(1; kw=1, ", - ) +# TODO: @test_nocompletion("CompletionFoo.kwtest4(x23=17; ") +# TODO: @test_nocompletion("CompletionFoo.kwtest4.(x23=17; ") + +let s = "CompletionFoo.kwtest5(3, somekwarg=6," c, r, res = test_complete(s) @test !res @test length(c) == 1 - @test occursin("a; x, y, w...", c[1]) + @test occursin("kwtest5(a::$(Int), b, x...; somekwarg, somekotherkwarg)", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.kwtest5(3, somekwarg=6, anything, ") end +# TODO: @test_nocompletion("CompletionFoo.kwtest5(3; somekwarg=6,") +# TODO: @test_nocompletion("CompletionFoo.kwtest5(3;") +# TODO: @test_nocompletion("CompletionFoo.kwtest5(3; somekwarg=6, anything, ") + ################################################################# # method completion with `?` (arbitrary method with given argument types) @@ -644,6 +692,36 @@ let s = "CompletionFoo.?()" @test occursin("test10(s::String...)", c[1]) end +#= TODO: restrict the number of completions when a semicolon is present in ".?(" syntax +let s = "CompletionFoo.?(; y=2, " + c, r, res = test_complete(s) + @test !res + @test length(c) == 4 + @test all(x -> occursin("kwtest", x), c) + # We choose to include kwtest2 and kwtest3 although the number of args if wrong. + # This is because the ".?(" syntax with no closing parenthesis does not constrain the + # number of arguments in the methods it suggests. +end + +let s = "CompletionFoo.?(3; len2=5, " + c, r, res = test_complete_noshift(s) + @test !res + @test length(c) == 1 + @test occursin("kwtest3(a::Integer; namedarg, foobar, slurp...)", c[1]) + # the other two kwtest3 methods should not appear because of specificity +end +=# + +# For the ".?(" syntax, do not constrain the number of arguments even with a semicolon. +@test test_complete("CompletionFoo.?(; ") == + test_complete("CompletionFoo.?(") + +#TODO: @test test_complete("CompletionFoo.?(Any[]...; ") == test_complete("CompletionFoo.?(Cmd[]..., ") == test_complete("CompletionFoo.?(") + +@test test_complete("CompletionFoo.?()") == test_complete("CompletionFoo.?(;)") + +#TODO: @test_nocompletion("CompletionFoo.?(3; len2=5; ") + ################################################################# # Test method completion with varargs @@ -753,6 +831,56 @@ let s = "CompletionFoo.test11('d', 3," @test any(str->occursin("test11(::Any, ::Any, s::String)", str), c) end +let s = "CompletionFoo.test!12(" + c, r, res = test_complete(s) + @test !res + @test occursin("test!12()", only(c)) +end + +#= TODO: Test method completion depending on the number of arguments with splatting + +@test_nocompletion("CompletionFoo.test3(unknown; ") +@test_nocompletion("CompletionFoo.test3.(unknown; ") + +let s = "CompletionFoo.test2(unknown..., somethingelse..., xyz...; " # splat may be empty + c, r, res = test_complete(s) + @test !res + @test length(c) == 3 + @test all(str->occursin("test2(", str), c) + @test (c, r, res) == test_complete("CompletionFoo.test2(unknown..., somethingelse..., xyz, ") + @test (c, r, res) == test_complete("CompletionFoo.test2(unknown..., somethingelse..., xyz; ") +end + +let s = "CompletionFoo.test('a', args..., 'b';" + c, r, res = test_complete(s) + @test !res + @test length(c) == 1 + @test occursin("test(args...)", c[1]) + @test (c, r, res) == test_complete("CompletionFoo.test(a, args..., b, c;") +end + +let s = "CompletionFoo.test(3, 5, args...,;" + c, r, res = test_complete(s) + @test !res + @test length(c) == 2 + @test any(str->occursin("test(x::T, y::T) where T<:Real", str), c) + @test any(str->occursin("test(args...)", str), c) +end +=# + +# Test that method calls with ill-formed kwarg syntax are not completed + +@test_nocompletion("CompletionFoo.kwtest(; x=2, y=4; kw=3, ") +@test_nocompletion("CompletionFoo.kwtest(x=2; y=4; ") +@test_nocompletion("CompletionFoo.kwtest((x=y)=4, ") +@test_nocompletion("CompletionFoo.kwtest(; (x=y)=4, ") +@test_nocompletion("CompletionFoo.kwtest(; w...=16, ") +@test_nocompletion("CompletionFoo.kwtest(; 2, ") +@test_nocompletion("CompletionFoo.kwtest(; 2=3, ") +@test_nocompletion("CompletionFoo.kwtest3(im; (true ? length : length), ") +@test_nocompletion("CompletionFoo.kwtest.(x=2; y=4; ") +@test_nocompletion("CompletionFoo.kwtest.(; w...=16, ") + # Test of inference based getfield completion let s = "(1+2im)." c,r = test_complete(s) @@ -789,6 +917,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) @@ -1260,6 +1395,129 @@ 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 + # TODO: @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 + # TODO: @test "_something=" ∉ c # no such keyword for the method with keyword `xαβγ` + c, r = test_complete("CompletionFoo.kwtest4.(a; xαβγ=1, _") + @test "_a1b=" ∈ c + # TODO: @test "_something=" ∉ c # broadcasting does not affect the existence of kwargs + c, r = test_complete("CompletionFoo.kwtest4(a; x23=0, x") + @test "x23=" ∉ c + # TODO: @test "xαβγ=" ∉ c + c, r = test_complete("CompletionFoo.kwtest4.(a; x23=0, x") + @test "x23=" ∉ c + # TODO: @test "xαβγ=" ∉ c + c, r = test_complete("CompletionFoo.kwtest4(a; _a1b=1, x") + @test "x23=" ∈ c + @test "xαβγ=" ∈ c + + c, r = test_complete("CompletionFoo.kwtest5(3, 5; somek") + @test c == ["somekotherkwarg=", "somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, somekwarg=4, somek") + @test c == ["somekotherkwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7; somekw") + @test c == ["somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7, 9; somekw") + @test c == ["somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7, 9, Any[]...; somek") + @test c == ["somekotherkwarg=", "somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(unknownsplat...; somekw") + @test c == ["somekwarg="] + c, r = test_complete("CompletionFoo.kwtest5(3, 5, 7, 9, somekwarg=4, somek") + @test c == ["somekotherkwarg="] + c, r = test_complete("CompletionFoo.kwtest5(String[]..., unknownsplat...; xy") + @test c == ["xyz="] + c, r = test_complete("CompletionFoo.kwtest5('a', unknownsplat...; xy") + @test c == ["xyz="] + c, r = test_complete("CompletionFoo.kwtest5('a', 3, String[]...; xy") + @test c == ["xyz="] + + # 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(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..., ") + + #= TODO: Test the absence of kwarg completion the call is incompatible with the method bearing the kwarg. + @test hasnokwsuggestions("CompletionFoo.kwtest3(a") + @test hasnokwsuggestions("CompletionFoo.kwtest3(le") + @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 + @test hasnokwsuggestions("CompletionFoo.kwtest3(a, len2=b, length, foob") # length is not length=length + @test hasnokwsuggestions("CompletionFoo.kwtest5('a', 3, 5, unknownsplat...; xy") + @test hasnokwsuggestions("CompletionFoo.kwtest5(3; somek") + =# +end + # Test completion in context # No CompletionFoo.CompletionFoo @@ -1374,6 +1632,8 @@ let s = "test.(1,1, " @test length(c) == 4 @test r == 1:4 @test s[r] == "test" + # TODO: @test (c, r, res) == test_complete_foo("test.(1, 1, String[]..., ") + # TODO: @test (c, r, res) == test_complete_foo("test.(1, Any[]..., 2, ") end let s = "prevind(\"θ\",1,"