summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--alot/commands/globals.py4
-rw-r--r--alot/completion.py222
2 files changed, 165 insertions, 61 deletions
diff --git a/alot/commands/globals.py b/alot/commands/globals.py
index f5b46ffd..43441763 100644
--- a/alot/commands/globals.py
+++ b/alot/commands/globals.py
@@ -87,7 +87,9 @@ class PromptCommand(Command):
text=self.startwith,
completer=CommandLineCompleter(ui.dbman,
ui.accountman,
- mode),
+ mode,
+ ui.current_buffer,
+ ),
history=ui.commandprompthistory,
)
ui.logger.debug('CMDLINE: %s' % cmdline)
diff --git a/alot/completion.py b/alot/completion.py
index 02dd8904..3d1a1b15 100644
--- a/alot/completion.py
+++ b/alot/completion.py
@@ -4,6 +4,7 @@ import glob
import logging
import alot.commands as commands
+from alot.buffers import EnvelopeBuffer
class Completer(object):
@@ -15,6 +16,7 @@ class Completer(object):
:param original: the string to complete
:type original: str
:param pos: starting position to complete from
+ :type pos: int
:returns: pairs of completed string and cursor position in the
new string
:rtype: list of (str, int)
@@ -33,6 +35,66 @@ class Completer(object):
return original[start:end], start, end, pos - start
+class StringlistCompleter(Completer):
+ """completer for a fixed list of strings"""
+
+ def __init__(self, resultlist):
+ """
+ :param resultlist: strings used for completion
+ :type accountman: list of str
+ """
+ self.resultlist = resultlist
+
+ def complete(self, original, pos):
+ pref = original[:pos]
+ return [(a, len(a)) for a in self.resultlist if a.startswith(pref)]
+
+
+class MultipleSelectionCompleter(Completer):
+ """
+ Meta-Completer that turns any Completer into one that deals with a list of
+ completion strings using the wrapped Completer.
+ This allows for example to easily construct a completer for comma separated
+ recipient-lists using a :class:`ContactsCompleter`.
+ """
+
+ def __init__(self, completer, separator=', '):
+ """
+ :param completer: completer to use for individual substrings
+ :type completer: Completer
+ :param separator: separator used to split the completion string into
+ substrings to be fed to `completer`.
+ :type separator: str
+ """
+ self._completer = completer
+ self._separator = separator
+
+ def relevant_part(self, original, pos):
+ """
+ calculates the subword of `original` that `pos` is in
+ """
+ start = original.rfind(self._separator, 0, pos)
+ if start == -1:
+ start = 0
+ else:
+ start = start + len(self._separator)
+ end = original.find(self._separator, pos - 1)
+ if end == -1:
+ end = len(original)
+ return original[start:end], start, end, pos - start
+
+ def complete(self, original, pos):
+ mypart, start, end, mypos = self.relevant_part(original, pos)
+ prefix = mypart[:mypos]
+ res = []
+ for c, p in self._completer.complete(mypart, mypos):
+ newprefix = original[:start] + c
+ if not original[end:].startswith(self._separator):
+ newprefix += self._separator
+ res.append((newprefix + original[end:], len(newprefix)))
+ return res
+
+
class QueryCompleter(Completer):
"""completion for a notmuch query string"""
def __init__(self, dbman, accountman):
@@ -45,8 +107,8 @@ class QueryCompleter(Completer):
"""
self.dbman = dbman
abooks = accountman.get_addressbooks()
- self._contactscompleter = ContactsCompleter(abooks, addressesonly=True)
- self._tagscompleter = TagsCompleter(dbman)
+ self._abookscompleter = AbooksCompleter(abooks, addressesonly=True)
+ self._tagcompleter = TagCompleter(dbman)
self.keywords = ['tag', 'from', 'to', 'subject', 'attachment',
'is', 'id', 'thread', 'folder']
@@ -58,10 +120,10 @@ class QueryCompleter(Completer):
cmd, params = m.groups()
cmdlen = len(cmd) + 1 # length of the keyword part incld colon
if cmd in ['to', 'from']:
- localres = self._contactscompleter.complete(mypart[cmdlen:],
- mypos - cmdlen)
+ localres = self._abookscompleter.complete(mypart[cmdlen:],
+ mypos - cmdlen)
else:
- localres = self._tagscompleter.complete(mypart[cmdlen:],
+ localres = self._tagcompleter.complete(mypart[cmdlen:],
mypos - cmdlen)
resultlist = []
for ltxt, lpos in localres:
@@ -78,7 +140,19 @@ class QueryCompleter(Completer):
return resultlist
-class TagsCompleter(Completer):
+class TagCompleter(StringlistCompleter):
+ """complete a tagstring"""
+
+ def __init__(self, dbman):
+ """
+ :param dbman: used to look up avaliable tagstrings
+ :type dbman: :class:`~alot.db.DBManager`
+ """
+ resultlist = dbman.get_all_tags()
+ StringlistCompleter.__init__(self, resultlist)
+
+
+class TagsCompleter(MultipleSelectionCompleter):
"""completion for a comma separated list of tagstrings"""
def __init__(self, dbman):
@@ -86,30 +160,26 @@ class TagsCompleter(Completer):
:param dbman: used to look up avaliable tagstrings
:type dbman: :class:`~alot.db.DBManager`
"""
- self.dbman = dbman
+ self._completer = TagCompleter(dbman)
+ self._separator = ','
- def complete(self, original, pos, single_tag=True):
- tags = self.dbman.get_all_tags()
- if single_tag:
- prefix = original[:pos]
- matching = [t for t in tags if t.startswith(prefix)]
- return [(t, len(t)) for t in matching]
- else:
- mypart, start, end, mypos = self.relevant_part(original, pos,
- sep=',')
- prefix = mypart[:mypos]
- res = []
- for tag in tags:
- if tag.startswith(prefix):
- newprefix = original[:start] + tag
- if not original[end:].startswith(','):
- newprefix += ','
- res.append((newprefix + original[end:], len(newprefix)))
- return res
-
-
-class ContactsCompleter(Completer):
- """completes contacts"""
+
+class ContactsCompleter(MultipleSelectionCompleter):
+ """completes contacts from given address books"""
+ def __init__(self, abooks, addressesonly=False):
+ """
+ :param abooks: used to look up email addresses
+ :type abooks: list of :class:`~alot.account.AddresBook`
+ :param addressesonly: only insert address, not the realname of the
+ contact
+ :type addressesonly: bool
+ """
+ self._completer = AbooksCompleter(abooks, addressesonly=addressesonly)
+ self._separator = ', '
+
+
+class AbooksCompleter(Completer):
+ """completes a contact from given address books"""
def __init__(self, abooks, addressesonly=False):
"""
:param abooks: used to look up email addresses
@@ -138,7 +208,7 @@ class ContactsCompleter(Completer):
return returnlist
-class AccountCompleter(Completer):
+class AccountCompleter(StringlistCompleter):
"""completes users' own mailaddresses"""
def __init__(self, accountman):
@@ -146,12 +216,8 @@ class AccountCompleter(Completer):
:param accountman: used to look up the list of addresses
:type accountman: :class:`~alot.account.AccountManager`
"""
- self.accountman = accountman
-
- def complete(self, original, pos):
- valids = self.accountman.get_main_addresses()
- prefix = original[:pos]
- return [(a, len(a)) for a in valids if a.startswith(prefix)]
+ resultlist = accountman.get_main_addresses()
+ StringlistCompleter.__init__(self, resultlist)
class CommandCompleter(Completer):
@@ -177,7 +243,7 @@ class CommandCompleter(Completer):
class CommandLineCompleter(Completer):
"""completion for commandline"""
- def __init__(self, dbman, accountman, mode):
+ def __init__(self, dbman, accountman, mode, currentbuffer=None):
"""
:param dbman: used to look up avaliable tagstrings
:type dbman: :class:`~alot.db.DBManager`
@@ -186,13 +252,18 @@ class CommandLineCompleter(Completer):
:type accountman: :class:`~alot.account.AccountManager`
:param mode: mode identifier
:type mode: str
+ :param currentbuffer: currently active buffer. If defined, this will be
+ used to dynamically extract possible completion
+ strings
+ :type currentbuffer: :class:`~alot.buffers.Buffer`
"""
self.dbman = dbman
self.accountman = accountman
self.mode = mode
+ self.currentbuffer = currentbuffer
self._commandcompleter = CommandCompleter(mode)
self._querycompleter = QueryCompleter(dbman, accountman)
- self._tagscompleter = TagsCompleter(dbman)
+ self._tagcompleter = TagCompleter(dbman)
abooks = accountman.get_addressbooks()
self._contactscompleter = ContactsCompleter(abooks)
self._pathcompleter = PathCompleter()
@@ -208,35 +279,66 @@ class CommandLineCompleter(Completer):
else:
cmd, params = words
localpos = pos - (len(cmd) + 1)
+ # set 'res' - the result set of matching completionstrings
+ # depending on the current mode and command
+
+ # global
if cmd == 'search':
res = self._querycompleter.complete(params, localpos)
- elif cmd == 'refine':
- if self.mode == 'search':
- res = self._querycompleter.complete(params, localpos)
- elif cmd == 'set' and self.mode == 'envelope':
- header, params = params.split(' ', 1)
- localpos = localpos - (len(header) + 1)
- if header.lower() in ['to', 'cc', 'bcc']:
-
- # prepend 'set ' + header and correct position
- def f((completed, pos)):
- return ('%s %s' % (header, completed),
- pos + len(header) + 1)
- res = map(f, self._contactscompleter.complete(params,
- localpos))
-
- logging.debug(res)
- elif cmd == 'retag':
- res = self._tagscompleter.complete(params, localpos,
- single_tag=False)
- elif cmd == 'toggletag':
- res = self._tagscompleter.complete(params, localpos)
elif cmd == 'help':
res = self._commandcompleter.complete(params, localpos)
elif cmd in ['compose']:
res = self._contactscompleter.complete(params, localpos)
- elif cmd in ['attach', 'edit', 'save']:
+ # search
+ elif self.mode == 'search' and cmd == 'refine':
+ res = self._querycompleter.complete(params, localpos)
+ elif self.mode == 'search' and cmd == 'retag':
+ localcomp = MultipleSelectionCompleter(self._tagcompleter,
+ separator=',')
+ res = localcomp.complete(params, localpos)
+ elif self.mode == 'search' and cmd == 'toggletag':
+ localcomp = MultipleSelectionCompleter(self._tagcompleter,
+ separator=' ')
+ res = localcomp.complete(params, localpos)
+ # envelope
+ elif self.mode == 'envelope' and cmd == 'set':
+ plist = params.split(' ', 1)
+ if len(plist) == 1: # complete from header keys
+ localprefix = params
+ headers = ['Subject', 'To', 'Cc', 'Bcc', 'In-Reply-To']
+ localcompleter = StringlistCompleter(headers)
+ localres = localcompleter.complete(localprefix, localpos)
+ res = [(c, p + 6) for (c, p) in localres]
+ else: # must have 2 elements
+ header, params = plist
+ localpos = localpos - (len(header) + 1)
+ if header.lower() in ['to', 'cc', 'bcc']:
+
+ # prepend 'set ' + header and correct position
+ def f((completed, pos)):
+ return ('%s %s' % (header, completed),
+ pos + len(header) + 1)
+ res = map(f, self._contactscompleter.complete(params,
+ localpos))
+ elif self.mode == 'envelope' and cmd == 'unset':
+ plist = params.split(' ', 1)
+ if len(plist) == 1: # complete from header keys
+ localprefix = params
+ buf = self.currentbuffer
+ if buf:
+ if isinstance(buf, EnvelopeBuffer):
+ available = buf.envelope.headers.keys()
+ localcompleter = StringlistCompleter(available)
+ localres = localcompleter.complete(localprefix,
+ localpos)
+ res = [(c, p + 6) for (c, p) in localres]
+
+ elif self.mode == 'envelope' and cmd == 'attach':
+ res = self._pathcompleter.complete(params, localpos)
+ # thread
+ elif self.mode == 'thread' and cmd == 'save':
res = self._pathcompleter.complete(params, localpos)
+
# prepend cmd and correct position
res = [('%s %s' % (cmd, t), p + len(cmd) + 1) for (t, p) in res]
return res