Skip to content

Commit

Permalink
[completion] Refactor input values into a CompletionApi object.
Browse files Browse the repository at this point in the history
In preparation for adding COMP_LINE, COMP_POINT, etc.

bash_completion uses these to reparse the input in another way!!!  Gah.
  • Loading branch information
Andy Chu committed Sep 29, 2018
1 parent d9aa20b commit 669429d
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 49 deletions.
13 changes: 7 additions & 6 deletions core/comp_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,24 +76,24 @@ class _SortedWordsAction(object):
def __init__(self, d):
self.d = d

def Matches(self, words, index, prefix):
def Matches(self, comp):
for name in sorted(self.d):
if name.startswith(prefix):
if name.startswith(comp.to_complete):
#yield name + ' ' # full word
yield name


class _DirectoriesAction(object):
"""complete -A directory"""

def Matches(self, words, index, prefix):
def Matches(self, comp):
raise NotImplementedError('-A directory')


class _UsersAction(object):
"""complete -A user"""

def Matches(self, words, index, prefix):
def Matches(self, comp):
raise NotImplementedError('-A user')


Expand Down Expand Up @@ -250,8 +250,9 @@ def CompGen(argv, ex):
# and also showing ALL COMPREPLY reuslts, not just the ones that start with
# the word to complete.
matched = False
for m in chain.Matches(['compgen', to_complete], -1, to_complete,
filter_func_matches=False):
comp = completion.CompletionApi(words=['compgen', to_complete], index=-1,
to_complete=to_complete)
for m in chain.Matches(comp, filter_func_matches=False):
matched = True
print(m)

Expand Down
105 changes: 62 additions & 43 deletions core/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,25 +56,16 @@ class _RetryCompletion(Exception):

class NullCompleter(object):

def Matches(self, words, index, to_complete):
def Matches(self, comp):
return []


_NULL_COMPLETER = NullCompleter()


class CompletionLookup(object):
"""
names -> list of actions
-E -> list of actions
-D -> default list of actions, when we don't know
Maybe call those __DEFAULT__ and '' or something, -D and -E is
confusing.
"""Stores completion hooks registered by the user."""

But I also want to register patterns.
"""
def __init__(self):
# command name -> ChainedCompleter
# There are pseudo commands __first and __fallback for -E and -D.
Expand Down Expand Up @@ -132,6 +123,37 @@ def GetCompleterForName(self, argv0):
return self.lookup['__fallback']


class CompletionApi(object):

def __init__(self, words=None, index=0, line='', point=0, to_complete='', key=0):
"""
Args:
index: if -1, then we're running through compgen
"""
self.words = words or [] # COMP_WORDS
self.index = index # COMP_CWORD
self.line = line # COMP_LINE
self.point = point # COMP_POINT
# the word to complete, which could be '' when words == []
self.to_complete = to_complete
self.key = key # COMP_KEY

# NOTE: COMP_WORDBREAKS is initliazed in Mem().

def GetApiInput(self):
"""Returns argv and comp_words."""

command = self.words[0]
if self.index == -1: # called directly by compgen, not by hitting TAB
prev = ''
comp_words = [] # not completing anything
else:
prev = '' if self.index == 0 else self.words[self.index - 1]
comp_words = self.words

return [command, self.to_complete, prev], comp_words


#
# Actions
#
Expand All @@ -145,7 +167,7 @@ class CompletionAction(object):
def __init__(self):
pass

def Matches(self, words, index, to_complete):
def Matches(self, comp):
pass


Expand All @@ -155,9 +177,9 @@ def __init__(self, words, delay=None):
self.words = words
self.delay = delay

def Matches(self, words, index, to_complete):
def Matches(self, comp):
for w in self.words:
if w.startswith(to_complete):
if w.startswith(comp.to_complete):
if self.delay:
time.sleep(self.delay)
yield w + ' '
Expand All @@ -170,7 +192,8 @@ class FileSystemAction(CompletionAction):
TODO: We need a variant that tests for an executable bit.
"""
def Matches(self, words, index, to_complete):
def Matches(self, comp):
to_complete = comp.to_complete
i = to_complete.rfind('/')
if i == -1: # it looks like 'foo'
to_list = '.'
Expand Down Expand Up @@ -198,6 +221,8 @@ def Matches(self, words, index, to_complete):


class ShellFuncAction(CompletionAction):
"""Call a user-defined function using bash's completion protocol."""

def __init__(self, ex, func):
self.ex = ex
self.func = func
Expand All @@ -209,21 +234,13 @@ def __repr__(self):
def log(self, *args):
self.ex.debug_f.log(*args)

def Matches(self, comp_words, index, to_complete):
def Matches(self, comp):
# TODO: Delete COMPREPLY here? It doesn't seem to be defined in bash by
# default.
command = comp_words[0]

if index == -1: # called directly by compgen, not by hitting TAB
prev = ''
comp_words = [] # not completing anything?
else:
prev = '' if index == 0 else comp_words[index-1]

argv = [command, to_complete, prev]
argv, comp_words = comp.GetApiInput()

state.SetGlobalArray(self.ex.mem, 'COMP_WORDS', comp_words)
state.SetGlobalString(self.ex.mem, 'COMP_CWORD', str(index))
state.SetGlobalString(self.ex.mem, 'COMP_CWORD', str(comp.index))

self.log('Running completion function %r with arguments %s',
self.func.name, argv)
Expand All @@ -233,7 +250,7 @@ def Matches(self, comp_words, index, to_complete):
self.log('Got status 124 from %r', self.func.name)
raise _RetryCompletion()

# Should be COMP_REPLY to follow naming convention! Lame.
# Lame: COMP_REPLY would follow the naming convention!
val = state.GetGlobal(self.ex.mem, 'COMPREPLY')
if val.tag == value_e.Undef:
util.error('Ran function %s but COMPREPLY was not defined', self.func.name)
Expand All @@ -254,7 +271,7 @@ class VariablesAction(object):
def __init__(self, mem):
self.mem = mem

def Matches(self, words, index, to_complete):
def Matches(self, comp):
for var_name in self.mem.VarNames():
yield var_name

Expand All @@ -267,7 +284,8 @@ class VariablesActionInternal(object):
def __init__(self, mem):
self.mem = mem

def Matches(self, words, index, to_complete):
def Matches(self, comp):
to_complete = comp.to_complete
assert to_complete.startswith('$')
to_complete = to_complete[1:]
for name in self.mem.VarNames():
Expand Down Expand Up @@ -305,7 +323,7 @@ def __init__(self, mem):
# huge, and will require lots of sys calls.
self.cache = {}

def Matches(self, words, index, to_complete):
def Matches(self, comp):
"""
TODO: Cache is never cleared.
Expand Down Expand Up @@ -336,7 +354,7 @@ def Matches(self, words, index, to_complete):
# TODO: Shouldn't do the prefix / space thing ourselves. readline does
# that at the END of the line.
for word in listing:
if word.startswith(to_complete):
if word.startswith(comp.to_complete):
yield word + ' '


Expand Down Expand Up @@ -373,15 +391,15 @@ def __init__(self, actions, predicate=None, prefix='', suffix=''):
self.prefix = prefix
self.suffix = suffix

def Matches(self, words, index, to_complete, filter_func_matches=True):
def Matches(self, comp, filter_func_matches=True):
# NOTE: This has to be evaluated eagerly so we get the _RetryCompletion
# exception.
for a in self.actions:
for match in a.Matches(words, index, to_complete):
for match in a.Matches(comp):
# Special case hack to match bash for compgen -F. It doesn't filter by
# to_complete!
show = (
match.startswith(to_complete) and self.predicate(match) or
match.startswith(comp.to_complete) and self.predicate(match) or
(isinstance(a, ShellFuncAction) and not filter_func_matches)
)

Expand Down Expand Up @@ -571,8 +589,7 @@ def _GetCompletionType(w_parser, c_parser, ev, debug_f):


def _GetCompletionType1(parser, buf):
words = parser.GetWords(buf)
comp_name = None
words = parser.GetWords(buf) # just does a dummy split for now

n = len(words)
# Complete variables
Expand All @@ -581,21 +598,21 @@ def _GetCompletionType1(parser, buf):
# rules are almost the same.
if n > 0 and words[-1].startswith('$'):
comp_type = completion_state_e.VAR_NAME
prefix = words[-1]
to_complete = words[-1]

# Otherwise complete words
elif n == 0:
comp_type = completion_state_e.FIRST
prefix = ''
to_complete = ''
elif n == 1:
comp_type = completion_state_e.FIRST
prefix = words[-1]
to_complete = words[-1]
else:
comp_type = completion_state_e.REST
prefix = words[-1]
to_complete = words[-1]

comp_index = len(words) - 1
return comp_type, prefix, words
return comp_type, to_complete, words


class RootCompleter(object):
Expand Down Expand Up @@ -653,6 +670,9 @@ def Matches(self, buf):
else:
comp_type, to_complete, comp_words = _GetCompletionType1(self.parser, buf)

index = len(comp_words) - 1 # COMP_CWORD is -1 when it's empty
comp = CompletionApi(words=comp_words, index=index, to_complete=to_complete)

if comp_type == completion_state_e.VAR_NAME:
# Non-user chain
chain = self.var_comp
Expand All @@ -678,11 +698,10 @@ def Matches(self, buf):
self.progress_f.Write('Completing %r ... (Ctrl-C to cancel)', buf)
start_time = time.time()

index = len(comp_words) - 1 # COMP_CWORD -1 when it's empty
self.debug_f.log('Using %s', chain)

i = 0
for m in chain.Matches(comp_words, index, to_complete):
for m in chain.Matches(comp):
# TODO: need to dedupe these
yield m
i += 1
Expand Down

0 comments on commit 669429d

Please sign in to comment.