Skip to content

Commit

Permalink
?(x, y)TAB completes methods accepting x, y (JuliaLang#38791)
Browse files Browse the repository at this point in the history
* ?(x, y)TAB completes methods accepting x, y

Closes JuliaLang#30052
xref JuliaLang#38704
xref JuliaLang#37993

Co-authored-by: Jameson Nash <[email protected]>
  • Loading branch information
timholy and vtjnash committed Aug 9, 2021
1 parent 7005b7d commit e87e30c
Show file tree
Hide file tree
Showing 6 changed files with 251 additions and 31 deletions.
6 changes: 6 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ Standard library changes

#### REPL

* ` ?(x, y` followed by TAB displays 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`. TAB requires that at least one
argument have a type more specific than `Any`; use SHIFT-TAB instead of TAB
to allow any compatible methods.

#### SparseArrays

#### Dates
Expand Down
119 changes: 98 additions & 21 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,54 @@ 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:
If you wonder which methods can be used with particular argument types, use `?` as the function name.
This shows an example of looking for functions in InteractiveUtils that accept a single string:

```julia-repl
julia> import UUIDs
julia> UUIDs.uuid[TAB]
uuid1 uuid4 uuid_version
julia> InteractiveUtils.?("somefile")[TAB]
edit(path::AbstractString) in InteractiveUtils at InteractiveUtils/src/editless.jl:197
less(file::AbstractString) in InteractiveUtils at InteractiveUtils/src/editless.jl:266
```

Fields for output from functions can also be completed:
This listed methods in the `InteractiveUtils` module that can be called on a string.
By default, this excludes methods where all arguments are typed as `Any`,
but you can see those too by holding down SHIFT-TAB instead of TAB:

```julia-repl
julia> split("","")[1].[TAB]
lastindex offset string
julia> InteractiveUtils.?("somefile")[SHIFT-TAB]
apropos(string) in REPL at REPL/src/docview.jl:796
clipboard(x) in InteractiveUtils at InteractiveUtils/src/clipboard.jl:64
code_llvm(f) in InteractiveUtils at InteractiveUtils/src/codeview.jl:221
code_native(f) in InteractiveUtils at InteractiveUtils/src/codeview.jl:243
edit(path::AbstractString) in InteractiveUtils at InteractiveUtils/src/editless.jl:197
edit(f) in InteractiveUtils at InteractiveUtils/src/editless.jl:225
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:266
less(f) in InteractiveUtils at InteractiveUtils/src/editless.jl:274
report_bug(kind) in InteractiveUtils at InteractiveUtils/src/InteractiveUtils.jl:391
separate_kwargs(args...; kwargs...) in InteractiveUtils at InteractiveUtils/src/macros.jl:7
```

The completion of fields for output from functions uses type inference, and it can only suggest
fields if the function is type stable.
You can also use ` ?("somefile")[TAB]` and look across all modules, but the method lists can be long.

Dictionary keys can also be tab completed:
By omitting the closing parenthesis, you can include functions that might require additional arguments:

```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> using Mmap
help?> Mmap.?("file",[TAB]
Mmap.Anonymous(name::String, readonly::Bool, create::Bool) in Mmap at Mmap/src/Mmap.jl:16
mmap(file::AbstractString) in Mmap at Mmap/src/Mmap.jl:245
mmap(file::AbstractString, ::Type{T}) where T<:Array in Mmap at Mmap/src/Mmap.jl:245
mmap(file::AbstractString, ::Type{T}, dims::Tuple{Vararg{Integer, N}}) where {T<:Array, N} in Mmap at Mmap/src/Mmap.jl:245
mmap(file::AbstractString, ::Type{T}, dims::Tuple{Vararg{Integer, N}}, offset::Integer; grow, shared) where {T<:Array, N} in Mmap at Mmap/src/Mmap.jl:245
mmap(file::AbstractString, ::Type{T}, len::Integer) where T<:Array in Mmap at Mmap/src/Mmap.jl:251
mmap(file::AbstractString, ::Type{T}, len::Integer, offset::Integer; grow, shared) where T<:Array in Mmap at Mmap/src/Mmap.jl:251
mmap(file::AbstractString, ::Type{T}, dims::Tuple{Vararg{Integer, N}}) where {T<:BitArray, N} in Mmap at Mmap/src/Mmap.jl:316
mmap(file::AbstractString, ::Type{T}, dims::Tuple{Vararg{Integer, N}}, offset::Integer; grow, shared) where {T<:BitArray, N} in Mmap at Mmap/src/Mmap.jl:316
mmap(file::AbstractString, ::Type{T}, len::Integer) where T<:BitArray in Mmap at Mmap/src/Mmap.jl:322
mmap(file::AbstractString, ::Type{T}, len::Integer, offset::Integer; grow, shared) where T<:BitArray in Mmap at Mmap/src/Mmap.jl:322
```

## Customizing Colors
Expand Down
16 changes: 16 additions & 0 deletions stdlib/REPL/src/LineEdit.jl
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ mutable struct PromptState <: ModeState
refresh_wait::Union{Timer,Nothing}
end

struct Modifiers
shift::Bool
end
Modifiers() = Modifiers(false)

options(s::PromptState) =
if isdefined(s.p, :repl) && isdefined(s.p.repl, :options)
# we can't test isa(s.p.repl, LineEditREPL) as LineEditREPL is defined
Expand Down Expand Up @@ -1907,6 +1912,10 @@ mode(s::PromptState) = s.p # ::Prompt
mode(s::SearchState) = @assert false
mode(s::PrefixSearchState) = s.histprompt.parent_prompt # ::Prompt

setmodifiers!(s::MIState, m::Modifiers) = setmodifiers!(mode(s), m)
setmodifiers!(p::Prompt, m::Modifiers) = setmodifiers!(p.complete, m)
setmodifiers!(c) = 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 +2183,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)
setmodifiers!(s, Modifiers(true))
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 +2223,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,
setmodifiers!,
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
modifiers::LineEdit.Modifiers
end
REPLCompletionProvider() = REPLCompletionProvider(LineEdit.Modifiers())
mutable struct ShellCompletionProvider <: CompletionProvider end
struct LatexCompletions <: CompletionProvider end

setmodifiers!(c::REPLCompletionProvider, m::LineEdit.Modifiers) = c.modifiers = m

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.modifiers.shift
# 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
c.modifiers = LineEdit.Modifiers()
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 !Base.isdeprecated(callee_module, name) && isdefined(callee_module, name)
func = getfield(callee_module, name)
if !isa(func, Module)
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, Module)
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), 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 e87e30c

Please sign in to comment.