Commit cfd66d5f authored by Jacek Sowiński's avatar Jacek Sowiński

Reorganize code for completers

1.Rename completers

topydo.ui.prompt.TopydoCompleter is now topydo.ui.prompt.PromptCompleter
topydo.lib.Completer is now topydo.ui.CompleterBase

2. Reuse CompleterBase code in prompt completer.
3. Sort completion suggestions
4. Introduce completion of due: dates in column completer.
5. Store subcommands for completers in cache (via lru_cache).
parent 0e2bbbb7
...@@ -23,7 +23,7 @@ import sys ...@@ -23,7 +23,7 @@ import sys
from topydo.lib.Config import config, ConfigError from topydo.lib.Config import config, ConfigError
_SUBCOMMAND_MAP = { SUBCOMMAND_MAP = {
'add': 'AddCommand', 'add': 'AddCommand',
'app': 'AppendCommand', 'app': 'AppendCommand',
'append': 'AppendCommand', 'append': 'AppendCommand',
...@@ -63,9 +63,9 @@ def get_subcommand(p_args): ...@@ -63,9 +63,9 @@ def get_subcommand(p_args):
""" """
Returns the class of the requested subcommand. An invalid p_subcommand Returns the class of the requested subcommand. An invalid p_subcommand
will result in an ImportError, since this is a programming mistake will result in an ImportError, since this is a programming mistake
(most likely an error in the _SUBCOMMAND_MAP). (most likely an error in the SUBCOMMAND_MAP).
""" """
classname = _SUBCOMMAND_MAP[p_subcommand] classname = SUBCOMMAND_MAP[p_subcommand]
modulename = 'topydo.commands.{}'.format(classname) modulename = 'topydo.commands.{}'.format(classname)
__import__(modulename, globals(), locals(), [classname], 0) __import__(modulename, globals(), locals(), [classname], 0)
...@@ -111,14 +111,14 @@ def get_subcommand(p_args): ...@@ -111,14 +111,14 @@ def get_subcommand(p_args):
if subcommand in alias_map: if subcommand in alias_map:
result, args = resolve_alias(subcommand, args[1:]) result, args = resolve_alias(subcommand, args[1:])
elif subcommand in _SUBCOMMAND_MAP: elif subcommand in SUBCOMMAND_MAP:
result = import_subcommand(subcommand) result = import_subcommand(subcommand)
args = args[1:] args = args[1:]
elif subcommand == 'help': elif subcommand == 'help':
try: try:
subcommand = args[1] subcommand = args[1]
if subcommand in _SUBCOMMAND_MAP: if subcommand in SUBCOMMAND_MAP:
args = [subcommand, 'help'] args = [subcommand, 'help']
return get_subcommand(args) return get_subcommand(args)
except IndexError: except IndexError:
...@@ -128,14 +128,14 @@ def get_subcommand(p_args): ...@@ -128,14 +128,14 @@ def get_subcommand(p_args):
p_command = config().default_command() p_command = config().default_command()
if p_command in alias_map: if p_command in alias_map:
result, args = resolve_alias(p_command, args) result, args = resolve_alias(p_command, args)
elif p_command in _SUBCOMMAND_MAP: elif p_command in SUBCOMMAND_MAP:
result = import_subcommand(p_command) result = import_subcommand(p_command)
# leave args unchanged # leave args unchanged
except IndexError: except IndexError:
p_command = config().default_command() p_command = config().default_command()
if p_command in alias_map: if p_command in alias_map:
result, args = resolve_alias(p_command, args) result, args = resolve_alias(p_command, args)
elif p_command in _SUBCOMMAND_MAP: elif p_command in SUBCOMMAND_MAP:
result = import_subcommand(p_command) result = import_subcommand(p_command)
return (result, args) return (result, args)
...@@ -14,34 +14,71 @@ ...@@ -14,34 +14,71 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
from topydo.Commands import _SUBCOMMAND_MAP import datetime
from functools import lru_cache
from topydo.Commands import SUBCOMMAND_MAP
from topydo.lib.Config import config from topydo.lib.Config import config
@lru_cache(maxsize=1)
def _get_subcmds(): def _get_subcmds():
subcmd_map = config().aliases() subcmd_map = config().aliases().copy()
subcmd_map.update(_SUBCOMMAND_MAP) subcmd_map.update(SUBCOMMAND_MAP)
return sorted(subcmd_map.keys()) return sorted(subcmd_map.keys())
def date_suggestions():
"""
Returns a list of relative date that is presented to the user as auto
complete suggestions.
"""
# don't use strftime, prevent locales to kick in
days_of_week = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday",
5: "Saturday",
6: "Sunday"
}
dates = [
'today',
'tomorrow',
]
# show days of week up to next week
dow = datetime.date.today().weekday()
for i in range(dow + 2 % 7, dow + 7):
dates.append(days_of_week[i % 7])
# and some more relative days starting from next week
dates += ["1w", "2w", "1m", "2m", "3m", "1y"]
return dates
class CompleterBase(object): class CompleterBase(object):
def __init__(self, p_todolist): def __init__(self, p_todolist):
self.todolist = p_todolist self.todolist = p_todolist
self._subcmds = _get_subcmds() self._all_subcmds = _get_subcmds()
def _complete_context(self, p_word): def _contexts(self, p_word):
completions = ['@' + context for context in self.todolist.contexts() if completions = ['@' + context for context in self.todolist.contexts() if
context.startswith(p_word[1:])] context.startswith(p_word[1:])]
return completions return sorted(completions)
def _complete_project(self, p_word): def _projects(self, p_word):
completions = ['+' + project for project in self.todolist.projects() if completions = ['+' + project for project in self.todolist.projects() if
project.startswith(p_word[1:])] project.startswith(p_word[1:])]
return completions return sorted(completions)
def _complete_subcmd(self, p_word): def _subcmds(self, p_word):
completions = [cmd for cmd in self._subcmds if completions = [cmd for cmd in self._all_subcmds if
cmd.startswith(p_word)] cmd.startswith(p_word)]
return completions return completions
...@@ -49,10 +86,10 @@ class CompleterBase(object): ...@@ -49,10 +86,10 @@ class CompleterBase(object):
completions = [] completions = []
if p_word.startswith('+'): if p_word.startswith('+'):
completions = self._complete_project(p_word) completions = self._projects(p_word)
elif p_word.startswith('@'): elif p_word.startswith('@'):
completions = self._complete_context(p_word) completions = self._contexts(p_word)
elif p_is_first_word: elif p_is_first_word:
completions = self._complete_subcmd(p_word) completions = self._subcmds(p_word)
return completions return completions
# Topydo - A todo.txt client written in Python.
# Copyright (C) 2017 Bram Schoenmakers <bram@topydo.org>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
This module provides a completer class that can be used by the Column UI
CommmandLineWidget.
"""
from topydo.lib.Config import config
from topydo.ui.CompleterBase import CompleterBase, date_suggestions
class ColumnCompleter(CompleterBase):
"""
Completer class that completes projects, contexts, dates and
subcommands designed to work with CommandLineWidget for column UI.
"""
def get_completions(self, p_word, p_is_first_word=False):
def dates(p_word, p_tag):
dates = []
for date in date_suggestions():
candidate = p_tag + ':' + date
if candidate.startswith(p_word):
dates.append(candidate)
return dates
due_tag = config().tag_due()
start_tag = config().tag_start()
if p_word.startswith(due_tag + ':'):
return dates(p_word, due_tag)
elif p_word.startswith(start_tag + ':'):
return dates(p_word, start_tag)
else:
return super().get_completions(p_word, p_is_first_word)
...@@ -26,7 +26,7 @@ from string import ascii_uppercase ...@@ -26,7 +26,7 @@ from string import ascii_uppercase
from topydo.Commands import get_subcommand from topydo.Commands import get_subcommand
from topydo.lib.Config import config, ConfigError from topydo.lib.Config import config, ConfigError
from topydo.lib.Completer import CompleterBase from topydo.ui.columns.ColumnCompleter import ColumnCompleter
from topydo.lib.Sorter import Sorter from topydo.lib.Sorter import Sorter
from topydo.lib.Filter import get_filter_list, RelevanceFilter, DependencyFilter from topydo.lib.Filter import get_filter_list, RelevanceFilter, DependencyFilter
from topydo.lib.Utils import get_terminal_size from topydo.lib.Utils import get_terminal_size
...@@ -124,7 +124,7 @@ class UIApplication(CLIApplicationBase): ...@@ -124,7 +124,7 @@ class UIApplication(CLIApplicationBase):
self.columns = urwid.Columns([], dividechars=0, self.columns = urwid.Columns([], dividechars=0,
min_width=config().column_width()) min_width=config().column_width())
completer = CompleterBase(self.todolist) completer = ColumnCompleter(self.todolist)
self.commandline = CommandLineWidget(completer, 'topydo> ') self.commandline = CommandLineWidget(completer, 'topydo> ')
self.keystate_widget = KeystateWidget() self.keystate_widget = KeystateWidget()
self.status_line = urwid.Columns([ self.status_line = urwid.Columns([
......
...@@ -21,7 +21,7 @@ import shlex ...@@ -21,7 +21,7 @@ import shlex
import sys import sys
from topydo.ui.CLIApplicationBase import CLIApplicationBase, error, GENERIC_HELP from topydo.ui.CLIApplicationBase import CLIApplicationBase, error, GENERIC_HELP
from topydo.ui.prompt.TopydoCompleter import TopydoCompleter from topydo.ui.prompt.PromptCompleter import PromptCompleter
from prompt_toolkit.shortcuts import prompt from prompt_toolkit.shortcuts import prompt
from prompt_toolkit.history import InMemoryHistory from prompt_toolkit.history import InMemoryHistory
...@@ -61,7 +61,7 @@ class PromptApplication(CLIApplicationBase): ...@@ -61,7 +61,7 @@ class PromptApplication(CLIApplicationBase):
""" """
self.todolist.erase() self.todolist.erase()
self.todolist.add_list(self.todofile.read()) self.todolist.add_list(self.todofile.read())
self.completer = TopydoCompleter(self.todolist) self.completer = PromptCompleter(self.todolist)
def run(self): def run(self):
""" Main entry function. """ """ Main entry function. """
......
...@@ -19,94 +19,38 @@ This module provides a completer class that can be used by the prompt provided ...@@ -19,94 +19,38 @@ This module provides a completer class that can be used by the prompt provided
by the prompt toolkit. by the prompt toolkit.
""" """
import datetime
import re import re
from prompt_toolkit.completion import Completer, Completion from prompt_toolkit.completion import Completer, Completion
from topydo.Commands import _SUBCOMMAND_MAP from topydo.ui.CompleterBase import CompleterBase, date_suggestions
from topydo.lib.Config import config from topydo.lib.Config import config
from topydo.lib.RelativeDate import relative_date_to_date from topydo.lib.RelativeDate import relative_date_to_date
def _subcommands(p_word_before_cursor):
""" Generator for subcommand name completion. """
sc_map = config().aliases()
sc_map.update(_SUBCOMMAND_MAP)
subcommands = [sc for sc in sorted(sc_map.keys()) if
sc.startswith(p_word_before_cursor)]
for command in subcommands:
yield Completion(command, -len(p_word_before_cursor))
def _dates(p_word_before_cursor): def _dates(p_word_before_cursor):
""" Generator for date completion. """ """ Generator for date completion. """
def _date_suggestions():
"""
Returns a list of relative date that is presented to the user as auto
complete suggestions.
"""
# don't use strftime, prevent locales to kick in
days_of_week = {
0: "Monday",
1: "Tuesday",
2: "Wednesday",
3: "Thursday",
4: "Friday",
5: "Saturday",
6: "Sunday"
}
dates = [
'today',
'tomorrow',
]
# show days of week up to next week
dow = datetime.date.today().weekday()
for i in range(dow + 2 % 7, dow + 7):
dates.append(days_of_week[i % 7])
# and some more relative days starting from next week
dates += ["1w", "2w", "1m", "2m", "3m", "1y"]
return dates
to_absolute = lambda s: relative_date_to_date(s).isoformat() to_absolute = lambda s: relative_date_to_date(s).isoformat()
start_value_pos = p_word_before_cursor.find(':') + 1 start_value_pos = p_word_before_cursor.find(':') + 1
value = p_word_before_cursor[start_value_pos:] value = p_word_before_cursor[start_value_pos:]
for reldate in _date_suggestions(): for reldate in date_suggestions():
if not reldate.startswith(value): if not reldate.startswith(value):
continue continue
yield Completion(reldate, -len(value), display_meta=to_absolute(reldate)) yield Completion(reldate, -len(value), display_meta=to_absolute(reldate))
class TopydoCompleter(Completer): class PromptCompleter(CompleterBase, Completer):
""" """
Completer class that completes projects, contexts, dates and Completer class that completes projects, contexts, dates and
subcommands. subcommands and is compatible with prompt toolkit.
""" """
def __init__(self, p_todolist): def _completion_generator(self, p_word, is_first_word):
self.todolist = p_todolist candidates = super().get_completions(p_word, is_first_word)
for candidate in candidates:
def _projects(self, p_word_before_cursor): yield Completion(candidate, -len(p_word))
""" Generator for project completion. """
projects = [p for p in self.todolist.projects() if
p.startswith(p_word_before_cursor[1:])]
for project in projects:
yield Completion("+" + project, -len(p_word_before_cursor))
def _contexts(self, p_word_before_cursor):
""" Generator for context completion. """
contexts = [c for c in self.todolist.contexts() if
c.startswith(p_word_before_cursor[1:])]
for context in contexts:
yield Completion("@" + context, -len(p_word_before_cursor))
def get_completions(self, p_document, _): def get_completions(self, p_document, _):
# include all characters except whitespaces (for + and @) # include all characters except whitespaces (for + and @)
...@@ -114,15 +58,9 @@ class TopydoCompleter(Completer): ...@@ -114,15 +58,9 @@ class TopydoCompleter(Completer):
is_first_word = not re.match(r'\s*\S+\s', is_first_word = not re.match(r'\s*\S+\s',
p_document.current_line_before_cursor) p_document.current_line_before_cursor)
if is_first_word: if word_before_cursor.startswith(config().tag_due() + ':'):
return _subcommands(word_before_cursor)
elif word_before_cursor.startswith('+'):
return self._projects(word_before_cursor)
elif word_before_cursor.startswith('@'):
return self._contexts(word_before_cursor)
elif word_before_cursor.startswith(config().tag_due() + ':'):
return _dates(word_before_cursor) return _dates(word_before_cursor)
elif word_before_cursor.startswith(config().tag_start() + ':'): elif word_before_cursor.startswith(config().tag_start() + ':'):
return _dates(word_before_cursor) return _dates(word_before_cursor)
else:
return [] return self._completion_generator(word_before_cursor, is_first_word)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment