Commit bb59771d authored by mathieui's avatar mathieui

Fix #2049 (get the current completed argument)

A command argument can now be completed even if it isn’t the last
one in the input.

- Add a new method Input.new_completion
    Almost like the old auto_completion method, except taht it takes
    another argument: argument_position, which is the argument to be
    completed.
- Methods using the old completion method still work
- All completion methods in poezio now use the new one if necessary
- Further details can be found in the docstring of new_completion
parent 2744234d
......@@ -91,24 +91,27 @@ Completions are a bit tricky, but it’s easy once you get used to it:
They take an **Input** (a _windows_ class) as a parameter, named the_input
everywhere in the sources. To effectively have a completion, you have to call
**the_input.auto_completion()** at the end of the function.
**the_input.auto_completion()** or **the_input.new_completion()** at the end
of the function.
.. code-block:: python
class Input(Win):
# …
def auto_completion(completion_list, after='', quote=True):
def auto_completion(completion_list, after='', quotify=True):
# …
def new_completion(completion_list, argument_position, after='', quotify=True):
# …
Set the input to iterate over _completion_list_ when the user hits tab, insert
**after** after the completed item, and surround the item with double quotes or
not.
There is no method to find the current argument in the input (although the
feature is planned), so you have to assume the current argument is the last,
and guess it by splitting the string an checking for end-space.
To find the current completed argument, use the **input.get_argument_position()**
method. You can then use new_completion() to select the argument to be completed.
You can look for examples in the sources, all the possible cases are
covered (single-argument, complex arguments with spaces, several arguments,
etc…)
etc…).
......@@ -182,7 +182,7 @@ def datetime_tuple(timestamp):
:return: The date.
:rtype: :py:class:`datetime.datetime`
>>> common.datetime_tuple('20130226T06:23:12')
>>> datetime_tuple('20130226T06:23:12')
datetime.datetime(2013, 2, 26, 8, 23, 12)
"""
timestamp = timestamp.split('.')[0]
......@@ -233,19 +233,82 @@ def shell_split(st):
>>> shell_split('"sdf 1" "toto 2"')
['sdf 1', 'toto 2']
"""
sh = shlex.shlex(st, posix=True)
sh.commenters = ''
sh.whitespace_split = True
sh.quotes = '"'
ret = list()
try:
sh = shlex.shlex(st)
ret = []
w = sh.get_token()
while w and w[2] is not None:
ret.append(w[2])
w = sh.get_token()
while w is not None:
ret.append(w)
w = sh.get_token()
return ret
except ValueError:
return st.split(" ")
return ret
def find_argument(pos, text, quoted=True):
"""
Split an input into a list of arguments, return the number of the
argument selected by pos.
If the position searched is outside the string, or in a space between words,
then it will return the position of an hypothetical new argument.
See the doctests of the two methods for example behaviors.
:param int pos: The position to search.
:param str text: The text to analyze.
:param quoted: Whether to take quotes into account or not.
:rtype: int
"""
if quoted:
return find_argument_quoted(pos, text)
else:
return find_argument_unquoted(pos, text)
def find_argument_quoted(pos, text):
"""
>>> find_argument_quoted(4, 'toto titi tata')
3
>>> find_argument_quoted(4, '"toto titi" tata')
0
>>> find_argument_quoted(8, '"toto" "titi tata"')
1
>>> find_argument_quoted(8, '"toto" "titi tata')
1
>>> find_argument_quoted(18, '"toto" "titi tata" ')
2
"""
sh = shlex.shlex(text)
count = -1
w = sh.get_token()
while w and w[2] is not None:
count += 1
if w[0] <= pos < w[1]:
return count
w = sh.get_token()
return count + 1
def find_argument_unquoted(pos, text):
"""
>>> find_argument_unquoted(2, 'toto titi tata')
0
>>> find_argument_unquoted(3, 'toto titi tata')
0
>>> find_argument_unquoted(6, 'toto titi tata')
1
>>> find_argument_unquoted(4, 'toto titi tata')
3
>>> find_argument_unquoted(25, 'toto titi tata')
3
"""
ret = text.split()
search = 0
argnum = 0
for i, elem in enumerate(ret):
elem_start = text.find(elem, search)
elem_end = elem_start + len(elem)
search = elem_end
if elem_start <= pos < elem_end:
return i
argnum = i
return argnum + 1
def parse_str_to_secs(duration=''):
"""
......@@ -286,7 +349,7 @@ def parse_secs_to_str(duration=0):
:rtype: :py:class:`str`
>>> parse_secs_to_str(3601)
1h1s
'1h1s'
"""
secs, mins, hours, days = 0, 0, 0, 0
result = ''
......@@ -370,3 +433,8 @@ def safeJID(*args, **kwargs):
return JID(*args, **kwargs)
except InvalidJID:
return JID('')
if __name__ == "__main__":
import doctest
doctest.testmod()
......@@ -1482,8 +1482,8 @@ class Core(object):
def completion_help(self, the_input):
"""Completion for /help."""
commands = list(self.commands.keys()) + list(self.current_tab().commands.keys())
return the_input.auto_completion(commands, ' ', quotify=False)
commands = sorted(self.commands.keys()) + sorted(self.current_tab().commands.keys())
return the_input.new_completion(commands, 1, quotify=False)
def command_runkey(self, arg):
"""
......@@ -1509,7 +1509,7 @@ class Core(object):
list_ = []
list_.extend(self.key_func.keys())
list_.extend(self.current_tab().key_func.keys())
return the_input.auto_completion(list_, '', quotify=False)
return the_input.new_completion(list_, 1, quotify=False)
def command_status(self, arg):
"""
......@@ -1548,7 +1548,8 @@ class Core(object):
"""
Completion of /status
"""
return the_input.auto_completion([status for status in possible_show], ' ', quotify=False)
if the_input.get_argument_position() == 1:
return the_input.new_completion([status for status in possible_show], 1, ' ', quotify=False)
def command_presence(self, arg):
"""
......@@ -1595,15 +1596,11 @@ class Core(object):
"""
Completion of /presence
"""
text = the_input.get_text()
args = text.split()
n = len(args)
if text.endswith(' '):
n += 1
if n == 2:
return the_input.auto_completion([jid for jid in roster.jids()], '')
elif n == 3:
return the_input.auto_completion([status for status in possible_show], '')
arg = the_input.get_argument_position()
if arg == 1:
return the_input.auto_completion([jid for jid in roster.jids()], '', quotify=True)
elif arg == 2:
return the_input.auto_completion([status for status in possible_show], '', quotify=True)
def command_theme(self, arg=''):
"""/theme <theme name>"""
......@@ -1631,7 +1628,7 @@ class Core(object):
theme_files = [name[:-3] for name in names if name.endswith('.py')]
if not 'default' in theme_files:
theme_files.append('default')
return the_input.auto_completion(theme_files, '', quotify=False)
return the_input.new_completion(theme_files, 1, '', quotify=False)
def command_win(self, arg):
"""
......@@ -1675,7 +1672,7 @@ class Core(object):
for tab in self.tabs:
l.extend(tab.matching_names())
l = [i[1] for i in l]
return the_input.auto_completion(l, ' ', quotify=False)
return the_input.new_completion(l, 1, '', quotify=False)
def command_move_tab(self, arg):
"""
......@@ -1692,7 +1689,7 @@ class Core(object):
except ValueError:
old_tab = None
for tab in self.tabs:
if not old_tab and value in safeJID(tab.get_name()).user:
if not old_tab and value == tab.get_name():
old_tab = tab
if not old_tab:
self.information("Tab %s does not exist" % args[0], "Error")
......@@ -1712,8 +1709,11 @@ class Core(object):
def completion_move_tab(self, the_input):
"""Completion for /move_tab"""
nodes = [safeJID(tab.get_name()).user for tab in self.tabs]
return the_input.auto_completion(nodes, ' ', quotify=True)
n = the_input.get_argument_position(quoted=True)
if n == 1:
nodes = [tab.get_name() for tab in self.tabs if tab]
nodes.remove('Roster')
return the_input.new_completion(nodes, 1, ' ', quotify=True)
def command_list(self, arg):
"""
......@@ -1741,7 +1741,7 @@ class Core(object):
tab.get_name() not in muc_serv_list:
muc_serv_list.append(safeJID(tab.get_name()).server)
if muc_serv_list:
return the_input.auto_completion(muc_serv_list, ' ', quotify=False)
return the_input.new_completion(muc_serv_list, 1, quotify=False)
def command_version(self, arg):
"""
......@@ -1770,12 +1770,11 @@ class Core(object):
def completion_version(self, the_input):
"""Completion for /version"""
n = len(the_input.get_text().split())
if n > 2 or (n == 2 and the_input.get_text().endswith(' ')):
n = the_input.get_argument_position(quoted=True)
if n >= 2:
return
comp = reduce(lambda x, y: x + [i for i in y], (jid.resources for jid in roster if len(jid)), [])
comp = (str(res.jid) for res in comp)
return the_input.auto_completion(sorted(comp), '', quotify=False)
comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
return the_input.new_completion(sorted(comp), 1, '', quotify=True)
def command_join(self, arg, histo_length=None):
"""
......@@ -1968,18 +1967,15 @@ class Core(object):
def completion_bookmark_local(self, the_input):
"""Completion for /bookmark_local"""
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
n = the_input.get_argument_position(quoted=True)
args = common.shell_split(the_input.text)
if len(args) == 1:
jid = safeJID('')
else:
jid = safeJID(args[1])
if len(args) > 2:
if n >= 2:
return
if len(args) == 1:
args.append('')
jid = safeJID(args[1])
if jid.server and (jid.resource or jid.full.endswith('/')):
tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
nicks = [tab.own_nick] if tab else []
......@@ -1992,10 +1988,10 @@ class Core(object):
if not nick in nicks:
nicks.append(nick)
jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
return the_input.auto_completion(jids_list, '')
return the_input.new_completion(jids_list, 1, quotify=True)
muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
muc_list.append('*')
return the_input.auto_completion(muc_list, '')
return the_input.new_completion(muc_list, 1, quotify=True)
def command_bookmark(self, arg=''):
"""
......@@ -2070,22 +2066,18 @@ class Core(object):
def completion_bookmark(self, the_input):
"""Completion for /bookmark"""
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
if len(args) == 1:
jid = safeJID('')
else:
jid = safeJID(args[1])
args = common.shell_split(the_input.text)
n = the_input.get_argument_position(quoted=True)
if len(args) == 2:
return the_input.auto_completion(['true', 'false'], '')
if len(args) == 3:
if n == 2:
return the_input.new_completion(['true', 'false'], 2, quotify=True)
if n >= 3:
return
if len(args) == 1:
args.append('')
jid = safeJID(args[1])
if jid.server and (jid.resource or jid.full.endswith('/')):
tab = self.get_tab_by_name(jid.bare, tabs.MucTab)
nicks = [tab.own_nick] if tab else []
......@@ -2098,10 +2090,11 @@ class Core(object):
if not nick in nicks:
nicks.append(nick)
jids_list = ['%s/%s' % (jid.bare, nick) for nick in nicks]
return the_input.auto_completion(jids_list, '')
return the_input.new_completion(jids_list, 1, quotify=True)
muc_list = [tab.get_name() for tab in self.tabs if isinstance(tab, tabs.MucTab)]
muc_list.sort()
muc_list.append('*')
return the_input.auto_completion(muc_list, '')
return the_input.new_completion(muc_list, 1, quotify=True)
def command_bookmarks(self, arg=''):
"""/bookmarks"""
......@@ -2133,7 +2126,7 @@ class Core(object):
def completion_remove_bookmark(self, the_input):
"""Completion for /remove_bookmark"""
return the_input.auto_completion([bm.jid for bm in bookmark.bookmarks], '')
return the_input.new_completion([bm.jid for bm in bookmark.bookmarks], 1, quotify=False)
def command_set(self, arg):
"""
......@@ -2170,27 +2163,24 @@ class Core(object):
def completion_set(self, the_input):
"""Completion for /set"""
text = the_input.get_text()
args = common.shell_split(text)
n = len(args)
empty = False
if text.endswith(' '):
n += 1
empty = True
if n == 2:
if not empty and '|' in args[1]:
args = common.shell_split(the_input.text)
n = the_input.get_argument_position(quoted=True)
if n >= len(args):
args.append('')
if n == 1:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if not plugin_name in self.plugin_manager.plugins:
return the_input.auto_completion([],'')
return the_input.new_completion([], n, quotify=True)
plugin = self.plugin_manager.plugins[plugin_name]
end_list = ['%s|%s' % (plugin_name, section) for section in plugin.config.sections()]
else:
end_list = config.options('Poezio')
elif n == 3:
elif n == 2:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if not plugin_name in self.plugin_manager.plugins:
return the_input.auto_completion([''],'')
return the_input.auto_completion([''], n, quotify=True)
plugin = self.plugin_manager.plugins[plugin_name]
end_list = plugin.config.options(section or plugin_name)
elif not config.has_option('Poezio', args[1]):
......@@ -2201,11 +2191,11 @@ class Core(object):
end_list = []
else:
end_list = [config.get(args[1], ''), '']
elif n == 4:
elif n == 3:
if '|' in args[1]:
plugin_name, section = args[1].split('|')[:2]
if not plugin_name in self.plugin_manager.plugins:
return the_input.auto_completion([],'')
return the_input.auto_completion([''], n, quotify=True)
plugin = self.plugin_manager.plugins[plugin_name]
end_list = [plugin.config.get(args[2], '', section or plugin_name), '']
else:
......@@ -2213,7 +2203,9 @@ class Core(object):
end_list = ['']
else:
end_list = [config.get(args[2], '', args[1]), '']
return the_input.auto_completion(end_list, '')
else:
return
return the_input.new_completion(end_list, n, quotify=True)
def command_server_cycle(self, arg=''):
"""
......@@ -2241,18 +2233,12 @@ class Core(object):
def completion_server_cycle(self, the_input):
"""Completion for /server_cycle"""
txt = the_input.get_text()
args = txt.split()
n = len(args)
if txt.endswith(' '):
n += 1
if n == 2:
serv_list = set()
for tab in self.tabs:
if isinstance(tab, tabs.MucTab):
serv = safeJID(tab.get_name()).server
serv_list.add(serv)
return the_input.auto_completion(list(serv_list), ' ')
serv_list = set()
for tab in self.tabs:
if isinstance(tab, tabs.MucTab):
serv = safeJID(tab.get_name()).server
serv_list.add(serv)
return the_input.new_completion(sorted(serv_list), 1, ' ')
def command_last_activity(self, arg):
"""
......@@ -2284,7 +2270,7 @@ class Core(object):
self.xmpp.plugin['xep_0012'].get_last_activity(jid, block=False, callback=callback)
def completion_last_activity(self, the_input):
return the_input.auto_completion([jid for jid in roster.jids()], '', quotify=False)
return the_input.new_completion([jid for jid in roster.jids()], 1, quotify=False)
def command_mood(self, arg):
"""
......@@ -2304,7 +2290,9 @@ class Core(object):
def completion_mood(self, the_input):
"""Completion for /mood"""
return the_input.auto_completion(list(pep.MOODS.keys()), '', quotify=False)
n = the_input.get_argument_position(quoted=True)
if n == 1:
return the_input.new_completion(sorted(pep.MOODS.keys()), 1, quotify=True)
def command_activity(self, arg):
"""
......@@ -2347,18 +2335,16 @@ class Core(object):
def completion_activity(self, the_input):
"""Completion for /activity"""
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
if n == 2:
return the_input.auto_completion(list(pep.ACTIVITIES.keys()), '', quotify=False)
elif n == 3:
n = the_input.get_argument_position(quoted=True)
args = common.shell_split(the_input.text)
if n == 1:
return the_input.new_completion(sorted(pep.ACTIVITIES.keys()), n, quotify=True)
elif n == 2:
if args[1] in pep.ACTIVITIES:
l = list(pep.ACTIVITIES[args[1]])
l.remove('category')
return the_input.auto_completion(l, '', quotify=False)
l.sort()
return the_input.new_completion(l, n, quotify=True)
def command_invite(self, arg):
"""/invite <to> <room> [reason]"""
......@@ -2372,19 +2358,16 @@ class Core(object):
def completion_invite(self, the_input):
"""Completion for /invite"""
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
if n == 2:
return the_input.auto_completion([jid for jid in roster.jids()], '')
elif n == 3:
n = the_input.get_argument_position(quoted=True)
if n == 1:
return the_input.new_completion(sorted(jid for jid in roster.jids()), n, quotify=True)
elif n == 2:
rooms = []
for tab in self.tabs:
if isinstance(tab, tabs.MucTab) and tab.joined:
rooms.append(tab.get_name())
return the_input.auto_completion(rooms, '')
rooms.sort()
return the_input.new_completion(rooms, n, '', quotify=True)
def command_decline(self, arg):
"""/decline <room@server.tld> [reason]"""
......@@ -2400,13 +2383,9 @@ class Core(object):
def completion_decline(self, the_input):
"""Completion for /decline"""
txt = the_input.get_text()
args = common.shell_split(txt)
n = len(args)
if txt.endswith(' '):
n += 1
if n == 2:
return the_input.auto_completion(list(self.pending_invites.keys()), '')
n = the_input.get_argument_position(quoted=True)
if n == 1:
return the_input.auto_completion(sorted(self.pending_invites.keys()), 1, '', quotify=True)
### Commands without a completion in this class ###
......@@ -2442,6 +2421,20 @@ class Core(object):
self.reset_curses()
sys.exit()
def completion_bind(self, the_input):
n = the_input.get_argument_position()
if n == 1:
args = [key for key in self.key_func if not key.startswith('_')]
elif n == 2:
args = [key for key in self.key_func]
else:
return
return the_input.new_completion(args, n, '', quotify=False)
return the_input
def command_bind(self, arg):
"""
Bind a key.
......@@ -2537,15 +2530,15 @@ class Core(object):
def completion_message(self, the_input):
"""Completion for /message"""
n = len(the_input.get_text().split())
if n > 2 or (n == 2 and the_input.get_text().endswith(' ')):
n = the_input.get_argument_position(quoted=True)
if n >= 2:
return
comp = reduce(lambda x, y: x + [i for i in y], (jid.resources for jid in roster if len(jid)), [])
comp = sorted((str(res.jid) for res in comp))
bares = sorted(contact.bare_jid for contact in roster if len(contact))
off = sorted(contact.bare_jid for contact in roster if not len(contact))
comp = reduce(lambda x, y: x + [i.jid for i in y], (roster[jid].resources for jid in roster.jids() if len(roster[jid])), [])
comp = sorted(comp)
bares = sorted(roster[contact].bare_jid for contact in roster.jids() if len(roster[contact]))
off = sorted(jid for jid in roster.jids() if jid not in bares)
comp = bares + comp + off
return the_input.auto_completion(comp, '', quotify=True)
return the_input.new_completion(comp, 1, '', quotify=True)
def command_xml_tab(self, arg=''):
"""/xml_tab"""
......@@ -2652,6 +2645,7 @@ class Core(object):
self.register_command('bind', self.command_bind,
usage=_(' <key> <equ>'),
desc=_('Bind a key to another key or to a “command”. For example "/bind ^H KEY_UP" makes Control + h do the same same as the Up key.'),
completion=self.completion_bind,
shortdesc=_('Bind a key to another key.'))
self.register_command('load', self.command_load,
usage=_('<plugin>'),
......@@ -3253,12 +3247,14 @@ class Core(object):
"""subscribe received"""
jid = presence['from'].bare
contact = roster[jid]
if contact.subscription in ('from', 'both'):
if contact and contact.subscription in ('from', 'both'):
return
elif contact.subscription == 'to':
elif contact and contact.subscription == 'to':
self.xmpp.sendPresence(pto=jid, ptype='subscribed')
self.xmpp.sendPresence(pto=jid)
else:
if not contact:
contact = roster.get_and_set(jid)
roster.update_contact_groups(contact)
contact.pending_in = True
self.information('%s wants to subscribe to your presence' % jid, 'Roster')
......
"""
Module containing the decorators
Module containing various decorators
"""
from functools import partial
class RefreshWrapper(object):
def __init__(self):
self.core = None
......@@ -40,5 +42,14 @@ class RefreshWrapper(object):
return ret
return wrap
def __completion(quoted, func):
class Completion(object):
quoted = quoted
def __new__(cls, *args, **kwargs):
return func(*args, **kwargs)
return Completion
completion_quotes = partial(__completion, True)
completion_raw = partial(__completion, False)
refresh_wrapper = RefreshWrapper()