Skip to content

Commit

Permalink
?(x, y)TAB completes methods accepting x, y
Browse files Browse the repository at this point in the history
Closes #30052
xref #38704
xref #37993
  • Loading branch information
timholy committed Jul 25, 2021
1 parent ffc340c commit b215082
Show file tree
Hide file tree
Showing 6 changed files with 229 additions and 38 deletions.
5 changes: 5 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,11 @@ Standard library changes

#### REPL

* ` ?(x, y` followed by tab returns all methods that can be called
with arguments `x, y, ...`. (The space at the beginning prevents entering help-mode.)
`MyModule.?(x, y` limits the search to `MyModule`. Using SHIFT-TAB instead of TAB
removes methods that have no constaints on their argument types.

#### SparseArrays

#### Dates
Expand Down
110 changes: 82 additions & 28 deletions stdlib/REPL/docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,27 @@ Users should refer to `LineEdit.jl` to discover the available actions on key inp
In both the Julian and help modes of the REPL, one can enter the first few characters of a function
or type and then press the tab key to get a list all matches:

```julia-repl
julia> x[TAB]
julia> xor
```

In some cases it only completes part of the name, up to the next ambiguity:

```julia-repl
julia> mapf[TAB]
julia> mapfold
```

If you hit tab again, then you get the list of things that might complete this:

```julia-repl
julia> mapfold[TAB]
mapfoldl mapfoldr
```

Like other components of the REPL, the search is case-sensitive:

```julia-repl
julia> stri[TAB]
stride strides string strip
Expand Down Expand Up @@ -365,6 +386,46 @@ shell> /[TAB]
.dockerinit bin/ dev/ home/ lib64/ mnt/ proc/ run/ srv/ tmp/ var/
```

Dictionary keys can also be tab completed:

```julia-repl
julia> foo = Dict("qwer1"=>1, "qwer2"=>2, "asdf"=>3)
Dict{String,Int64} with 3 entries:
"qwer2" => 2
"asdf" => 3
"qwer1" => 1
julia> foo["q[TAB]
"qwer1" "qwer2"
julia> foo["qwer
```

Tab completion can also help completing fields:

```julia-repl
julia> x = 3 + 4im;
julia> julia> x.[TAB][TAB]
im re
julia> import UUIDs
julia> UUIDs.uuid[TAB][TAB]
uuid1 uuid4 uuid5 uuid_version
```

Fields for output from functions can also be completed:

```julia-repl
julia> split("","")[1].[TAB]
lastindex offset string
```

The completion of fields for output from functions uses type inference, and it can only suggest
fields if the function is type stable.


Tab completion can help with investigation of the available methods matching the input arguments:

```julia-repl
Expand Down Expand Up @@ -392,38 +453,31 @@ The completion of the methods uses type inference and can therefore see if the a
even if the arguments are output from functions. The function needs to be type stable for the
completion to be able to remove non-matching methods.

Tab completion can also help completing fields:

```julia-repl
julia> import UUIDs
julia> UUIDs.uuid[TAB]
uuid1 uuid4 uuid_version
```

Fields for output from functions can also be completed:
If you wonder which methods can be used with particular argument types, use `?` as the function name:

```julia-repl
julia> split("","")[1].[TAB]
lastindex offset string
```

The completion of fields for output from functions uses type inference, and it can only suggest
fields if the function is type stable.

Dictionary keys can also be tab completed:
julia> InteractiveUtils.?("somefile")[TAB]
apropos(string) in REPL at REPL/src/docview.jl:727
clipboard(x) in InteractiveUtils at InteractiveUtils/src/clipboard.jl:60
code_llvm(f) in InteractiveUtils at InteractiveUtils/src/codeview.jl:178
code_native(f) in InteractiveUtils at InteractiveUtils/src/codeview.jl:199
edit(path::AbstractString) in InteractiveUtils at InteractiveUtils/src/editless.jl:195
edit(f) in InteractiveUtils at InteractiveUtils/src/editless.jl:223
eval(x) in InteractiveUtils at InteractiveUtils/src/InteractiveUtils.jl:3
include(x) in InteractiveUtils at InteractiveUtils/src/InteractiveUtils.jl:3
less(file::AbstractString) in InteractiveUtils at InteractiveUtils/src/editless.jl:256
less(f) in InteractiveUtils at InteractiveUtils/src/editless.jl:264
report_bug(kind) in InteractiveUtils at InteractiveUtils/src/InteractiveUtils.jl:385
separate_kwargs(args...; kwargs...) in InteractiveUtils at InteractiveUtils/src/macros.jl:7
```

This listed all methods in the `InteractiveUtils` module that can be called on a string.
If you use SHIFT-TAB instead of TAB, you exclude methods that have all arguments typed as `Any`:

```julia-repl
julia> foo = Dict("qwer1"=>1, "qwer2"=>2, "asdf"=>3)
Dict{String,Int64} with 3 entries:
"qwer2" => 2
"asdf" => 3
"qwer1" => 1
julia> foo["q[TAB]
"qwer1" "qwer2"
julia> foo["qwer
julia> InteractiveUtils.?("hi")[SHIFT-TAB]
edit(path::AbstractString) in InteractiveUtils at InteractiveUtils/src/editless.jl:195
less(file::AbstractString) in InteractiveUtils at InteractiveUtils/src/editless.jl:256
```

## Customizing Colors
Expand Down
11 changes: 11 additions & 0 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1907,6 +1907,10 @@ mode(s::PromptState) = s.p # ::Prompt
mode(s::SearchState) = @assert false
mode(s::PrefixSearchState) = s.histprompt.parent_prompt # ::Prompt

setmodifier!(s::MIState, val::Symbol) = setmodifier!(mode(s), val)
setmodifier!(p::Prompt, val::Symbol) = setmodifier!(p.complete, val)
setmodifier!(c, val::Symbol) = nothing

# Search Mode completions
function complete_line(s::SearchState, repeats)
completions, partial, should_complete = complete_line(s.histprompt.complete, s)
Expand Down Expand Up @@ -2174,6 +2178,11 @@ function edit_tab(s::MIState, jump_spaces::Bool=false, delete_trailing::Bool=jum
return refresh_line(s)
end

function shift_tab_completion(s::MIState)
setmodifier!(s, :shift)
return complete_line(s)
end

# return true iff the content of the buffer is modified
# return false when only the position changed
function edit_insert_tab(buf::IOBuffer, jump_spaces::Bool=false, delete_trailing::Bool=jump_spaces)
Expand Down Expand Up @@ -2209,6 +2218,8 @@ const default_keymap =
AnyDict(
# Tab
'\t' => (s::MIState,o...)->edit_tab(s, true),
# Shift-tab
"\e[Z" => (s::MIState,o...)->shift_tab_completion(s),
# Enter
'\r' => (s::MIState,o...)->begin
if on_enter(s) || (eof(buffer(s)) && s.key_repeats > 1)
Expand Down
17 changes: 16 additions & 1 deletion stdlib/REPL/src/REPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import ..LineEdit:
history_last,
history_search,
accept_result,
setmodifier!,
terminal,
MIState,
PromptState,
Expand Down Expand Up @@ -470,16 +471,30 @@ LineEditREPL(t::TextTerminal, hascolor::Bool, envcolors::Bool=false) =
false, false, false, envcolors
)

mutable struct REPLCompletionProvider <: CompletionProvider end
mutable struct REPLCompletionProvider <: CompletionProvider
modifier::Symbol
end
REPLCompletionProvider() = REPLCompletionProvider(:none)
mutable struct ShellCompletionProvider <: CompletionProvider end
struct LatexCompletions <: CompletionProvider end

setmodifier!(c::REPLCompletionProvider, val::Symbol) = c.modifier = val

beforecursor(buf::IOBuffer) = String(buf.data[1:buf.ptr-1])

function complete_line(c::REPLCompletionProvider, s::PromptState)
partial = beforecursor(s.input_buffer)
full = LineEdit.input_string(s)
ret, range, should_complete = completions(full, lastindex(partial))
if c.modifier === :shift
c.modifier = :none
# Filter out methods where all arguments are `Any`
filter!(ret) do c
isa(c, REPLCompletions.MethodCompletion) || return true
sig = Base.unwrap_unionall(c.method.sig)::DataType
return !all(T -> T === Any || T === Vararg{Any}, sig.parameters[2:end])
end
end
return unique!(map(completion_text, ret)), partial[range], should_complete
end

Expand Down
94 changes: 85 additions & 9 deletions stdlib/REPL/src/REPLCompletions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -478,17 +478,59 @@ function get_type(sym, fn::Module)
return found ? Core.Typeof(val) : Any, found
end

function get_type(T, found::Bool, default_any::Bool)
return found ? T :
default_any ? Any : throw(ArgumentError("argument not found"))
end

# Method completion on function call expression that look like :(max(1))
function complete_methods(ex_org::Expr, context_module::Module=Main)
func, found = get_value(ex_org.args[1], context_module)::Tuple{Any,Bool}
!found && return Completion[]

funargs = ex_org.args[2:end]
# handle broadcasting, but only handle number of arguments instead of
# argument types
args_ex, kwargs_ex = complete_methods_args(ex_org.args[2:end], ex_org, context_module, true, true)

out = Completion[]
complete_methods!(out, func, args_ex, kwargs_ex)
return out
end

function complete_any_methods(ex_org::Expr, callee_module::Module, context_module::Module, moreargs::Bool)
out = Completion[]
args_ex, kwargs_ex = try
complete_methods_args(ex_org.args[2:end], ex_org, context_module, false, false)
catch
return out
end

for name in names(callee_module; all=true)
if isdefined(callee_module, name)
func = getfield(callee_module, name)
if isa(func, Base.Callable) && func !== Vararg
complete_methods!(out, func, args_ex, kwargs_ex, moreargs)
elseif callee_module === Main::Module && isa(func, Module)
callee_module2 = func
for name in names(callee_module2)
if isdefined(callee_module2, name)
func = getfield(callee_module, name)
if isa(func, Base.Callable) && func !== Vararg
complete_methods!(out, func, args_ex, kwargs_ex, moreargs)
end
end
end
end
end
end

return out
end

function complete_methods_args(funargs::Vector{Any}, ex_org::Expr, context_module::Module, default_any::Bool, allow_broadcasting::Bool)
args_ex = Any[]
kwargs_ex = Pair{Symbol,Any}[]
if ex_org.head === :. && ex_org.args[2] isa Expr
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
Expand All @@ -497,18 +539,20 @@ function complete_methods(ex_org::Expr, context_module::Module=Main)
if isexpr(ex, :parameters)
for x in ex.args
n, v = isexpr(x, :kw) ? (x.args...,) : (x, x)
push!(kwargs_ex, n => first(get_type(v, context_module)))
push!(kwargs_ex, n => get_type(get_type(v, context_module)..., default_any))
end
elseif isexpr(ex, :kw)
n, v = (ex.args...,)
push!(kwargs_ex, n => first(get_type(v, context_module)))
push!(kwargs_ex, n => get_type(get_type(v, context_module)..., default_any))
else
push!(args_ex, first(get_type(ex, context_module)))
push!(args_ex, get_type(get_type(ex, context_module)..., default_any))
end
end
end
return args_ex, kwargs_ex
end

out = Completion[]
function complete_methods!(out::Vector{Completion}, @nospecialize(func::Base.Callable), args_ex::Vector{Any}, kwargs_ex::Vector{Pair{Symbol,Any}}, moreargs::Bool=true)
ml = methods(func)
# Input types and number of arguments
if isempty(kwargs_ex)
Expand All @@ -525,6 +569,9 @@ function complete_methods(ex_org::Expr, context_module::Module=Main)
ml = methods(kwfunc)
func = kwfunc
end
if !moreargs
na = typemax(Int)
end

for (method::Method, orig_method) in zip(ml, orig_ml)
ms = method.sig
Expand All @@ -534,7 +581,6 @@ function complete_methods(ex_org::Expr, context_module::Module=Main)
push!(out, MethodCompletion(func, t_in, method, orig_method))
end
end
return out
end

include("latex_symbols.jl")
Expand Down Expand Up @@ -652,6 +698,36 @@ function completions(string::String, pos::Int, context_module::Module=Main)
partial = string[1:pos]
inc_tag = Base.incomplete_tag(Meta.parse(partial, raise=false, depwarn=false))

# _(x, y)TAB lists methods you can call with these objects
# _(x, y TAB lists methods that take these objects as the first two arguments
# MyModule._(x, y)TAB restricts the search to names in MyModule
rexm = match(r"(\w+\.|)\?\((.*)$", partial)
if rexm !== nothing
# Get the module scope
if isempty(rexm.captures[1])
callee_module = context_module
else
modname = Symbol(rexm.captures[1][1:end-1])
if isdefined(context_module, modname)
callee_module = getfield(context_module, modname)
if !isa(callee_module, Module)
callee_module = context_module
end
else
callee_module = context_module
end
end
moreargs = !endswith(rexm.captures[2], ')')
callstr = "_(" * rexm.captures[2]
if moreargs
callstr *= ')'
end
ex_org = Meta.parse(callstr, raise=false, depwarn=false)
if isa(ex_org, Expr)
return complete_any_methods(ex_org, callee_module::Module, context_module, moreargs), (0:length(rexm.captures[1])+1) .+ rexm.offset, false
end
end

# if completing a key in a Dict
identifier, partial_key, loc = dict_identifier_key(partial, inc_tag, context_module)
if identifier !== nothing
Expand Down
Loading

0 comments on commit b215082

Please sign in to comment.