Merge pull request #900 from funkyfuture/completions.d

Load autocompletions from directory
This commit is contained in:
Anthony Scopatz 2016-05-17 22:04:05 -04:00
commit 60e4750980

View file

@ -5,6 +5,7 @@ import re
import ast import ast
import sys import sys
import shlex import shlex
from pathlib import Path
import pickle import pickle
import inspect import inspect
import builtins import builtins
@ -40,7 +41,7 @@ def _path_from_partial_string(inp, pos=None):
else: else:
return None return None
string = partial[startix:endix] string = partial[startix:endix]
end = re.sub(RE_STRING_START,'',quote) end = re.sub(RE_STRING_START, '', quote)
_string = string _string = string
if not _string.endswith(end): if not _string.endswith(end):
_string = _string + end _string = _string + end
@ -84,6 +85,7 @@ for ((i=0;i<${{#COMPREPLY[*]}};i++)) do echo ${{COMPREPLY[i]}}; done
WS = set(' \t\r\n') WS = set(' \t\r\n')
def startswithlow(x, start, startlow=None): def startswithlow(x, start, startlow=None):
"""True if x starts with a string or its lowercase version. The lowercase """True if x starts with a string or its lowercase version. The lowercase
version may be optionally be provided. version may be optionally be provided.
@ -120,6 +122,7 @@ def _normpath(p):
return p return p
class Completer(object): class Completer(object):
"""This provides a list of optional completions for the xonsh shell.""" """This provides a list of optional completions for the xonsh shell."""
@ -268,8 +271,9 @@ class Completer(object):
return {max(i, key=len) for i in reps.values()} return {max(i, key=len) for i in reps.values()}
def find_and_complete(self, line, idx, ctx=None): def find_and_complete(self, line, idx, ctx=None):
"""Finds the completions given only the full code line and a current cursor """Finds the completions given only the full code line and a current
position. This represents an easier alternative to the complete() method. cursor position. This represents an easier alternative to the
complete() method.
Parameters Parameters
---------- ----------
@ -308,8 +312,8 @@ class Completer(object):
csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS') csc = builtins.__xonsh_env__.get('CASE_SENSITIVE_COMPLETIONS')
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
key = prefix[1:] key = prefix[1:]
keylow = key.lower() paths.update({'$' + k for k in builtins.__xonsh_env__
paths.update({'$' + k for k in builtins.__xonsh_env__ if startswither(k, key, keylow)}) if startswither(k, key, key.lower())})
def _add_dots(self, paths, prefix): def _add_dots(self, paths, prefix):
if prefix in {'', '.'}: if prefix in {'', '.'}:
@ -353,7 +357,6 @@ class Completer(object):
else: else:
return single return single
def _quote_paths(self, paths, start, end): def _quote_paths(self, paths, start, end):
out = set() out = set()
space = ' ' space = ' '
@ -386,7 +389,6 @@ class Completer(object):
out.add(start + s + end) out.add(start + s + end)
return out return out
def path_complete(self, prefix, start, end, cdpath=False): def path_complete(self, prefix, start, end, cdpath=False):
"""Completes based on a path name.""" """Completes based on a path name."""
tilde = '~' tilde = '~'
@ -425,38 +427,40 @@ class Completer(object):
else: else:
prefix = shlex.quote(prefix) prefix = shlex.quote(prefix)
script = BASH_COMPLETE_SCRIPT.format(filename=fnme, script = BASH_COMPLETE_SCRIPT.format(
line=' '.join(shlex.quote(p) for p in splt), filename=fnme, line=' '.join(shlex.quote(p) for p in splt),
comp_line=shlex.quote(line), n=n, func=func, cmd=cmd, comp_line=shlex.quote(line), n=n, func=func, cmd=cmd,
end=endidx + 1, prefix=prefix, prev=shlex.quote(prev)) end=endidx + 1, prefix=prefix, prev=shlex.quote(prev))
try: try:
out = subprocess.check_output(['bash'], input=script, out = subprocess.check_output(
universal_newlines=True, stderr=subprocess.PIPE, ['bash'], input=script, universal_newlines=True,
env=builtins.__xonsh_env__.detype()) stderr=subprocess.PIPE, env=builtins.__xonsh_env__.detype())
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
out = '' out = ''
rtn = set(out.splitlines()) rtn = set(out.splitlines())
return rtn return rtn
def _source_completions(self): @staticmethod
srcs = [] def _collect_completions_sources():
for f in builtins.__xonsh_env__.get('BASH_COMPLETIONS'): sources = []
if os.path.isfile(f): paths = (Path(x) for x in
# We need to "Unixify" Windows paths for Bash to understand builtins.__xonsh_env__.get('BASH_COMPLETIONS'))
if ON_WINDOWS: for path in paths:
f = RE_WIN_DRIVE.sub(lambda m: '/{0}/'.format(m.group(1).lower()), f).replace('\\', '/') if path.is_file():
srcs.append('source ' + f) sources.append('source ' + str(path))
return srcs elif path.is_dir():
for _file in (x for x in path.glob('*') if x.is_file()):
sources.append('source ' + str(_file))
return sources
def _load_bash_complete_funcs(self): def _load_bash_complete_funcs(self):
self.bash_complete_funcs = bcf = {} self.bash_complete_funcs = bcf = {}
inp = self._source_completions() inp = self._collect_completions_sources()
if len(inp) == 0: if not inp:
return return
inp.append('complete -p\n') inp.append('complete -p\n')
out = subprocess.check_output(['bash'], input='\n'.join(inp), out = self._source_completions(inp)
env=builtins.__xonsh_env__.detype(), universal_newlines=True)
for line in out.splitlines(): for line in out.splitlines():
head, _, cmd = line.rpartition(' ') head, _, cmd = line.rpartition(' ')
if len(cmd) == 0 or cmd == 'cd': if len(cmd) == 0 or cmd == 'cd':
@ -467,8 +471,8 @@ class Completer(object):
bcf[cmd] = m.group(1) bcf[cmd] = m.group(1)
def _load_bash_complete_files(self): def _load_bash_complete_files(self):
inp = self._source_completions() inp = self._collect_completions_sources()
if len(inp) == 0: if not inp:
self.bash_complete_files = {} self.bash_complete_files = {}
return return
if self.bash_complete_funcs: if self.bash_complete_funcs:
@ -476,8 +480,7 @@ class Completer(object):
bash_funcs = set(self.bash_complete_funcs.values()) bash_funcs = set(self.bash_complete_funcs.values())
inp.append('declare -F ' + ' '.join([f for f in bash_funcs])) inp.append('declare -F ' + ' '.join([f for f in bash_funcs]))
inp.append('shopt -u extdebug\n') inp.append('shopt -u extdebug\n')
out = subprocess.check_output(['bash'], input='\n'.join(inp), out = self._source_completions(inp)
env=builtins.__xonsh_env__.detype(), universal_newlines=True)
func_files = {} func_files = {}
for line in out.splitlines(): for line in out.splitlines():
parts = line.split() parts = line.split()
@ -488,6 +491,11 @@ class Completer(object):
if func in func_files if func in func_files
} }
def _source_completions(self, source):
return subprocess.check_output(
['bash'], input='\n'.join(source), universal_newlines=True,
env=builtins.__xonsh_env__.detype(), stderr=subprocess.DEVNULL)
def attr_complete(self, prefix, ctx): def attr_complete(self, prefix, ctx):
"""Complete attributes of an object.""" """Complete attributes of an object."""
attrs = set() attrs = set()
@ -513,10 +521,11 @@ class Completer(object):
opts = [] opts = []
for i in _opts: for i in _opts:
try: try:
v = eval('{0}.{1}'.format(expr, i), _ctx) eval('{0}.{1}'.format(expr, i), _ctx)
opts.append(i)
except: # pylint:disable=bare-except except: # pylint:disable=bare-except
continue continue
else:
opts.append(i)
if len(attr) == 0: if len(attr) == 0:
opts = [o for o in opts if not o.startswith('_')] opts = [o for o in opts if not o.startswith('_')]
else: else:
@ -558,11 +567,12 @@ class ManCompleter(object):
startswither = startswithnorm if csc else startswithlow startswither = startswithnorm if csc else startswithlow
if cmd not in self._options.keys(): if cmd not in self._options.keys():
try: try:
manpage = subprocess.Popen(["man", cmd], manpage = subprocess.Popen(
stdout=subprocess.PIPE, stderr=subprocess.DEVNULL) ["man", cmd], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL)
# This is a trick to get rid of reverse line feeds # This is a trick to get rid of reverse line feeds
text = subprocess.check_output(["col", "-b"], text = subprocess.check_output(
stdin=manpage.stdout) ["col", "-b"], stdin=manpage.stdout)
text = text.decode('utf-8') text = text.decode('utf-8')
scraped_text = ' '.join(SCRAPE_RE.findall(text)) scraped_text = ' '.join(SCRAPE_RE.findall(text))
matches = INNER_OPTIONS_RE.findall(scraped_text) matches = INNER_OPTIONS_RE.findall(scraped_text)