Revision 762cdf14382f4979df6ff9f909d0af9f053082dc authored by Tim Besard on 23 September 2016, 13:55:00 UTC, committed by Tim Besard on 23 September 2016, 13:55:00 UTC
1 parent e60961b
REPLCompletions.jl
# This file is a part of Julia. License is MIT: http://julialang.org/license
module REPLCompletions
export completions, shell_completions, bslash_completions
using Base.Meta
function completes_global(x, name)
return startswith(x, name) && !('#' in x)
end
function filtered_mod_names(ffunc::Function, mod::Module, name::AbstractString, all::Bool=false, imported::Bool=false)
ssyms = names(mod, all, imported)
filter!(ffunc, ssyms)
syms = String[string(s) for s in ssyms]
filter!(x->completes_global(x, name), syms)
end
# REPL Symbol Completions
function complete_symbol(sym, ffunc)
# Maybe be smarter in the future
context_module = Main
mod = context_module
name = sym
lookup_module = true
t = Union{}
if rsearch(sym, non_identifier_chars) < rsearch(sym, '.')
# Find module
lookup_name, name = rsplit(sym, ".", limit=2)
ex = Base.syntax_deprecation_warnings(false) do
parse(lookup_name, raise=false)
end
b, found = get_value(ex, context_module)
if found
if isa(b, Module)
mod = b
lookup_module = true
elseif Base.isstructtype(typeof(b))
lookup_module = false
t = typeof(b)
end
else # If the value is not found using get_value, the expression contain an advanced expression
lookup_module = false
t, found = get_type(ex, context_module)
end
found || return String[]
# Ensure REPLCompletion do not crash when asked to complete a tuple, #15329
!lookup_module && t <: Tuple && return String[]
end
suggestions = String[]
if lookup_module
# We will exclude the results that the user does not want, as well
# as excluding Main.Main.Main, etc., because that's most likely not what
# the user wants
p = s->(!Base.isdeprecated(mod, s) && s != module_name(mod) && ffunc(mod, s))
# Looking for a binding in a module
if mod == context_module
# Also look in modules we got through `using`
mods = ccall(:jl_module_usings, Any, (Any,), Main)
for m in mods
append!(suggestions, filtered_mod_names(p, m, name))
end
append!(suggestions, filtered_mod_names(p, mod, name, true, true))
else
append!(suggestions, filtered_mod_names(p, mod, name, true, false))
end
else
# Looking for a member of a type
fields = fieldnames(t)
for field in fields
s = string(field)
if startswith(s, name)
push!(suggestions, s)
end
end
end
suggestions
end
function complete_keyword(s::String)
const sorted_keywords = [
"abstract", "baremodule", "begin", "bitstype", "break", "catch", "ccall",
"const", "continue", "do", "else", "elseif", "end", "export", "false",
"finally", "for", "function", "global", "if", "immutable", "import",
"importall", "let", "local", "macro", "module", "quote", "return",
"true", "try", "type", "typealias", "using", "while"]
r = searchsorted(sorted_keywords, s)
i = first(r)
n = length(sorted_keywords)
while i <= n && startswith(sorted_keywords[i],s)
r = first(r):i
i += 1
end
sorted_keywords[r]
end
function complete_path(path::AbstractString, pos; use_envpath=false)
if Base.is_unix() && ismatch(r"^~(?:/|$)", path)
# if the path is just "~", don't consider the expanded username as a prefix
if path == "~"
dir, prefix = homedir(), ""
else
dir, prefix = splitdir(homedir() * path[2:end])
end
else
dir, prefix = splitdir(path)
end
local files
try
if isempty(dir)
files = readdir()
elseif isdir(dir)
files = readdir(dir)
else
return String[], 0:-1, false
end
catch
return String[], 0:-1, false
end
matches = Set{String}()
for file in files
if startswith(file, prefix)
id = try isdir(joinpath(dir, file)) catch; false end
# joinpath is not used because windows needs to complete with double-backslash
push!(matches, id ? file * (@static is_windows() ? "\\\\" : "/") : file)
end
end
if use_envpath && length(dir) == 0
# Look for files in PATH as well
local pathdirs = split(ENV["PATH"], @static is_windows() ? ";" : ":")
for pathdir in pathdirs
local actualpath
try
actualpath = realpath(pathdir)
catch
# Bash doesn't expect every folder in PATH to exist, so neither shall we
continue
end
if actualpath != pathdir && in(actualpath,pathdirs)
# Remove paths which (after resolving links) are in the env path twice.
# Many distros eg. point /bin to /usr/bin but have both in the env path.
continue
end
local filesinpath
try
filesinpath = readdir(pathdir)
catch e
# Bash allows dirs in PATH that can't be read, so we should as well.
if isa(e, SystemError)
continue
else
# We only handle SystemErrors here
rethrow(e)
end
end
for file in filesinpath
# In a perfect world, we would filter on whether the file is executable
# here, or even on whether the current user can execute the file in question.
if startswith(file, prefix) && isfile(joinpath(pathdir, file))
push!(matches, file)
end
end
end
end
matchList = String[replace(s, r"\s", "\\ ") for s in matches]
startpos = pos - endof(prefix) + 1 - length(matchall(r" ", prefix))
# The pos - endof(prefix) + 1 is correct due to `endof(prefix)-endof(prefix)==0`,
# hence we need to add one to get the first index. This is also correct when considering
# pos, because pos is the `endof` a larger string which `endswith(path)==true`.
return matchList, startpos:pos, !isempty(matchList)
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=')')
braces = 0
r = RevString(s)
i = start(r)
in_single_quotes = false
in_double_quotes = false
in_back_ticks = false
while !done(r, i)
c, i = next(r, i)
if !in_single_quotes && !in_double_quotes && !in_back_ticks
if c == c_start
braces += 1
elseif c == c_end
braces -= 1
elseif c == '\''
in_single_quotes = true
elseif c == '"'
in_double_quotes = true
elseif c == '`'
in_back_ticks = true
end
else
if !in_back_ticks && !in_double_quotes && c == '\'' && !done(r, i) && next(r, i)[1]!='\\'
in_single_quotes = !in_single_quotes
elseif !in_back_ticks && !in_single_quotes && c == '"' && !done(r, i) && next(r, i)[1]!='\\'
in_double_quotes = !in_double_quotes
elseif !in_single_quotes && !in_double_quotes && c == '`' && !done(r, i) && next(r, i)[1]!='\\'
in_back_ticks = !in_back_ticks
end
end
braces == 1 && break
end
braces != 1 && return 0:-1, -1
method_name_end = reverseind(r, i)
startind = nextind(s, rsearch(s, non_identifier_chars, method_name_end))
return (startind:endof(s), method_name_end)
end
# Returns the value in a expression if sym is defined in current namespace fn.
# This method is used to iterate to the value of a expression like:
# :(Base.REPLCompletions.whitespace_chars) a `dump` of this expression
# will show it consist of Expr, QuoteNode's and Symbol's which all needs to
# be handled differently to iterate down to get the value of whitespace_chars.
function get_value(sym::Expr, fn)
sym.head != :. && return (nothing, false)
for ex in sym.args
fn, found = get_value(ex, fn)
!found && return (nothing, false)
end
return (fn, true)
end
get_value(sym::Symbol, fn) = isdefined(fn, sym) ? (getfield(fn, sym), true) : (nothing, false)
get_value(sym::QuoteNode, fn) = isdefined(fn, sym.value) ? (getfield(fn, sym.value), true) : (nothing, false)
get_value(sym, fn) = (sym, true)
# Return the value of a getfield call expression
function get_value_getfield(ex::Expr, fn)
# Example :((top(getfield))(Base,:max))
val, found = get_value_getfield(ex.args[2],fn) #Look up Base in Main and returns the module
found || return (nothing, false)
return get_value_getfield(ex.args[3], val) #Look up max in Base and returns the function if found.
end
get_value_getfield(sym, fn) = get_value(sym, fn)
# Determines the return type with Base.return_types of a function call using the type information of the arguments.
function get_type_call(expr::Expr)
f_name = expr.args[1]
# The if statement should find the f function. How f is found depends on how f is referenced
if isa(f_name, GlobalRef) && isconst(f_name.mod,f_name.name) && isdefined(f_name.mod,f_name.name)
ft = typeof(eval(f_name))
found = true
else
ft, found = get_type(f_name, Main)
end
found || return (Any, false) # If the function f is not found return Any.
args = Any[]
for ex in expr.args[2:end] # Find the type of the function arguments
typ, found = get_type(ex, Main)
found ? push!(args, typ) : push!(args, Any)
end
# use _methods_by_ftype as the function is supplied as a type
mt = Base._methods_by_ftype(Tuple{ft, args...}, -1)
length(mt) == 1 || return (Any, false)
m = first(mt)
# Typeinference
return_type = Core.Inference.typeinf_type(m[3], m[1], m[2])
return_type === nothing && return (Any, false)
return (return_type, true)
end
# Returns the return type. example: get_type(:(Base.strip("",' ')),Main) returns (String,true)
function get_type(sym::Expr, fn)
sym=expand(sym)
val, found = get_value(sym, fn)
found && return Base.typesof(val).parameters[1], found
if sym.head === :call
# getfield call is special cased as the evaluation of getfield provides good type information,
# is inexpensive and it is also performed in the complete_symbol function.
a1 = sym.args[1]
if isa(a1,GlobalRef) && isconst(a1.mod,a1.name) && isdefined(a1.mod,a1.name) &&
eval(a1) === Core.getfield
val, found = get_value_getfield(sym, Main)
return found ? Base.typesof(val).parameters[1] : Any, found
end
return get_type_call(sym)
end
return (Any, false)
end
function get_type(sym, fn)
val, found = get_value(sym, fn)
return found ? Base.typesof(val).parameters[1] : Any, found
end
# Method completion on function call expression that look like :(max(1))
function complete_methods(ex_org::Expr)
args_ex = DataType[]
func, found = get_value(ex_org.args[1], Main)
!found && return String[]
for ex in ex_org.args[2:end]
val, found = get_type(ex, Main)
push!(args_ex, val)
end
out = String[]
t_in = Tuple{Core.Typeof(func), args_ex...} # Input types
na = length(args_ex)+1
ml = methods(func)
kwtype = isdefined(ml.mt, :kwsorter) ? Nullable{DataType}(typeof(ml.mt.kwsorter)) : Nullable{DataType}()
io = IOBuffer()
for method in ml
# Check if the method's type signature intersects the input types
if typeintersect(Tuple{method.sig.parameters[1 : min(na, end)]...}, t_in) != Union{}
show(io, method, kwtype=kwtype)
push!(out, takebuf_string(io))
end
end
return out
end
include("latex_symbols.jl")
include("emoji_symbols.jl")
const non_identifier_chars = [" \t\n\r\"\\'`\$><=:;|&{}()[],+-*/?%^~"...]
const whitespace_chars = [" \t\n\r"...]
# "\"'`"... is added to whitespace_chars as non of the bslash_completions
# characters contain any of these characters. It prohibits the
# bslash_completions function to try and complete on escaped characters in strings
const bslash_separators = [whitespace_chars..., "\"'`"...]
# Aux function to detect whether we're right after a
# using or import keyword
function afterusing(string::String, startpos::Int)
(isempty(string) || startpos == 0) && return false
str = string[1:prevind(string,startpos)]
isempty(str) && return false
rstr = reverse(str)
r = search(rstr, r"\s(gnisu|tropmi)\b")
isempty(r) && return false
fr = reverseind(str, last(r))
return ismatch(r"^\b(using|import)\s*(\w+\s*,\s*)*\w*$", str[fr:end])
end
function bslash_completions(string, pos)
slashpos = rsearch(string, '\\', pos)
if (rsearch(string, bslash_separators, pos) < slashpos &&
!(1 < slashpos && (string[prevind(string, slashpos)]=='\\')))
# latex / emoji symbol substitution
s = string[slashpos:pos]
latex = get(latex_symbols, s, "")
if !isempty(latex) # complete an exact match
return (true, ([latex], slashpos:pos, true))
end
emoji = get(emoji_symbols, s, "")
if !isempty(emoji)
return (true, ([emoji], slashpos:pos, true))
end
# return possible matches; these cannot be mixed with regular
# Julian completions as only latex / emoji symbols contain the leading \
if startswith(s, "\\:") # emoji
emoji_names = filter(k -> startswith(k, s), keys(emoji_symbols))
return (true, (sort!(collect(emoji_names)), slashpos:pos, true))
else # latex
latex_names = filter(k -> startswith(k, s), keys(latex_symbols))
return (true, (sort!(collect(latex_names)), slashpos:pos, true))
end
end
return (false, (String[], 0:-1, false))
end
function dict_identifier_key(str,tag)
if tag === :string
str_close = str*"\""
elseif tag === :cmd
str_close = str*"`"
else
str_close = str
end
frange, end_of_indentifier = find_start_brace(str_close, c_start='[', c_end=']')
isempty(frange) && return (nothing, nothing, nothing)
obj = Main
for name in split(str[frange[1]:end_of_indentifier], '.')
Base.isidentifier(name) || return (nothing, nothing, nothing)
sym = Symbol(name)
isdefined(obj, sym) || return (nothing, nothing, nothing)
obj = getfield(obj, sym)
# Avoid `isdefined(::Array, ::Symbol)`
isa(obj, Array) && return (nothing, nothing, nothing)
end
begin_of_key = findnext(x->!in(x,whitespace_chars), str, end_of_indentifier+2)
begin_of_key==0 && return (true, nothing, nothing)
partial_key = str[begin_of_key:end]
(isa(obj, Associative) && length(obj) < 1e6) || return (true, nothing, nothing)
return (obj, partial_key, begin_of_key)
end
function completions(string, pos)
# First parse everything up to the current position
partial = string[1:pos]
inc_tag = Base.syntax_deprecation_warnings(false) do
Base.incomplete_tag(parse(partial, raise=false))
end
# if completing a key in a Dict
identifier, partial_key, loc = dict_identifier_key(partial,inc_tag)
if identifier !== nothing
if partial_key !== nothing
matches = []
for key in keys(identifier)
rkey = repr(key)
startswith(rkey,partial_key) && push!(matches,rkey)
end
length(matches)==1 && (length(string) <= pos || string[pos+1] != ']') && (matches[1]*="]")
length(matches)>0 && return sort(matches), loc:pos, true
else
return String[], 0:-1, false
end
end
# otherwise...
if inc_tag in [:cmd, :string]
m = match(r"[\t\n\r\"'`@\$><=;|&\{]| (?!\\)", reverse(partial))
startpos = nextind(partial, reverseind(partial, m.offset))
r = startpos:pos
paths, r, success = complete_path(replace(string[r], r"\\ ", " "), pos)
if inc_tag == :string &&
length(paths) == 1 && # Only close if there's a single choice,
!isdir(expanduser(replace(string[startpos:start(r)-1] * paths[1], r"\\ ", " "))) && # except if it's a directory
(length(string) <= pos || string[pos+1] != '"') # or there's already a " at the cursor.
paths[1] *= "\""
end
#Latex symbols can be completed for strings
(success || inc_tag==:cmd) && return sort(paths), r, success
end
ok, ret = bslash_completions(string, pos)
ok && return ret
# Make sure that only bslash_completions is working on strings
inc_tag==:string && return String[], 0:-1, false
if inc_tag == :other && should_method_complete(partial)
frange, method_name_end = find_start_brace(partial)
ex = Base.syntax_deprecation_warnings(false) do
parse(partial[frange] * ")", raise=false)
end
if isa(ex, Expr) && ex.head==:call
return complete_methods(ex), start(frange):method_name_end, false
end
elseif inc_tag == :comment
return String[], 0:-1, false
end
dotpos = rsearch(string, '.', pos)
startpos = nextind(string, rsearch(string, non_identifier_chars, pos))
ffunc = (mod,x)->true
suggestions = String[]
comp_keywords = true
if afterusing(string, startpos)
# We're right after using or import. Let's look only for packages
# and modules we can reach from here
# If there's no dot, we're in toplevel, so we should
# also search for packages
s = string[startpos:pos]
if dotpos <= startpos
for dir in [Pkg.dir(); LOAD_PATH; pwd()]
isdir(dir) || continue
for pname in readdir(dir)
if pname[1] != '.' && pname != "METADATA" &&
pname != "REQUIRE" && startswith(pname, s)
# Valid file paths are
# <Mod>.jl
# <Mod>/src/<Mod>.jl
# <Mod>.jl/src/<Mod>.jl
if isfile(joinpath(dir, pname))
endswith(pname, ".jl") && push!(suggestions, pname[1:end-3])
else
mod_name = if endswith(pname, ".jl")
pname[1:end - 3]
else
pname
end
if isfile(joinpath(dir, pname, "src",
"$mod_name.jl"))
push!(suggestions, mod_name)
end
end
end
end
end
end
ffunc = (mod,x)->(isdefined(mod, x) && isa(getfield(mod, x), Module))
comp_keywords = false
end
startpos == 0 && (pos = -1)
dotpos < startpos && (dotpos = startpos - 1)
s = string[startpos:pos]
comp_keywords && append!(suggestions, complete_keyword(s))
# The case where dot and start pos is equal could look like: "(""*"").d","". or CompletionFoo.test_y_array[1].y
# This case can be handled by finding the begining of the expresion. This is done bellow.
if dotpos == startpos
i = prevind(string, startpos)
while 0 < i
c = string[i]
if c in [')', ']']
if c==')'
c_start='('; c_end=')'
elseif c==']'
c_start='['; c_end=']'
end
frange, end_of_indentifier = find_start_brace(string[1:prevind(string, i)], c_start=c_start, c_end=c_end)
startpos = start(frange)
i = prevind(string, startpos)
elseif c in ["\'\"\`"...]
s = "$c$c"*string[startpos:pos]
break
else
break
end
s = string[startpos:pos]
end
end
append!(suggestions, complete_symbol(s, ffunc))
return sort(unique(suggestions)), (dotpos+1):pos, true
end
function shell_completions(string, pos)
# First parse everything up to the current position
scs = string[1:pos]
local args, last_parse
try
args, last_parse = Base.shell_parse(scs, true)
catch
return String[], 0:-1, false
end
# Now look at the last thing we parsed
isempty(args.args[end].args) && return String[], 0:-1, false
arg = args.args[end].args[end]
if all(s -> isa(s, AbstractString), args.args[end].args)
# Treat this as a path
# As Base.shell_parse throws away trailing spaces (unless they are escaped),
# we need to special case here.
# If the last char was a space, but shell_parse ignored it search on "".
ignore_last_word = arg != " " && scs[end] == ' '
prefix = ignore_last_word ? "" : join(args.args[end].args)
# Also try looking into the env path if the user wants to complete the first argument
use_envpath = !ignore_last_word && length(args.args) < 2
return complete_path(prefix, pos, use_envpath=use_envpath)
elseif isexpr(arg, :escape) && (isexpr(arg.args[1], :incomplete) || isexpr(arg.args[1], :error))
r = first(last_parse):prevind(last_parse, last(last_parse))
partial = scs[r]
ret, range = completions(partial, endof(partial))
range += first(r) - 1
return ret, range, true
end
return String[], 0:-1, false
end
end # module
![swh spinner](/static/img/swh-spinner.gif)
Computing file changes ...