600 lines
22 KiB
Julia
600 lines
22 KiB
Julia
# This file is a part of Julia. License is MIT: https://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 type", "baremodule", "begin", "break", "catch", "ccall",
|
|
"const", "continue", "do", "else", "elseif", "end", "export", "false",
|
|
"finally", "for", "function", "global", "if", "import",
|
|
"importall", "let", "local", "macro", "module", "mutable struct",
|
|
"primitive type", "quote", "return", "struct",
|
|
"true", "try", "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
|
|
world = ccall(:jl_get_world_counter, UInt, ())
|
|
mt = Base._methods_by_ftype(Tuple{ft, args...}, -1, world)
|
|
length(mt) == 1 || return (Any, false)
|
|
m = first(mt)
|
|
# Typeinference
|
|
params = Core.Inference.InferenceParams(world)
|
|
return_type = Core.Inference.typeinf_type(m[3], m[1], m[2], true, params)
|
|
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 = Any[]
|
|
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
|
|
ms = method.sig
|
|
|
|
# Do not suggest the default method from sysimg.jl.
|
|
if Base.is_default_method(method)
|
|
continue
|
|
end
|
|
|
|
# Check if the method's type signature intersects the input types
|
|
if typeintersect(Base.rewrap_unionall(Tuple{Base.unwrap_unionall(ms).parameters[1 : min(na, end)]...}, ms), t_in) != Union{}
|
|
show(io, method, kwtype=kwtype)
|
|
push!(out, String(take!(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 = Iterators.filter(k -> startswith(k, s), keys(emoji_symbols))
|
|
return (true, (sort!(collect(emoji_names)), slashpos:pos, true))
|
|
else # latex
|
|
latex_names = Iterators.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
|
|
|
|
# This needs to be a separate non-inlined function, see #19441
|
|
@noinline function find_dict_matches(identifier, partial_key)
|
|
matches = []
|
|
for key in keys(identifier)
|
|
rkey = repr(key)
|
|
startswith(rkey,partial_key) && push!(matches,rkey)
|
|
end
|
|
return matches
|
|
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 = find_dict_matches(identifier, partial_key)
|
|
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()]
|
|
dir isa AbstractString && 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, :incomplete) || isexpr(arg, :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
|