completion: Add tab completion for TaskWikiMod

This commit is contained in:
Tomas Janousek 2020-12-26 12:27:54 +01:00 committed by Tomas Babej
parent a6907bac85
commit 6cedc13b58
10 changed files with 243 additions and 4 deletions

View file

@ -32,3 +32,7 @@ function! taskwiki#FoldText()
let len_text = ' ['.fold_len.'] '
return short_text.len_text.repeat(' ', 500)
endfunction
function! taskwiki#CompleteMod(arglead, line, pos) abort
return py3eval('cache().get_relevant_completion().modify(vim.eval("a:arglead"))')
endfunction

View file

@ -604,6 +604,8 @@ selected tasks.
*:TaskWikiMod* [mods]
Opens a prompt for task modification, for selected task(s).
Supports |cmdline-completion|.
----------------------------------------------------------------------------
Interactive commands.

View file

@ -83,8 +83,8 @@ execute "command! -buffer -range TaskWikiDone :<line1>,<line2>" . g:taskwiki_p
execute "command! -buffer -range TaskWikiRedo :<line1>,<line2>" . g:taskwiki_py . "SelectedTasks().redo()"
execute "command! -buffer -range -nargs=* TaskWikiSort :<line1>,<line2>" . g:taskwiki_py . "SelectedTasks().sort(<q-args>)"
execute "command! -buffer -range -nargs=* TaskWikiMod :<line1>,<line2>" . g:taskwiki_py . "SelectedTasks().modify(<q-args>)"
execute "command! -buffer -range -nargs=* TaskWikiAnnotate :<line1>,<line2>" . g:taskwiki_py . "SelectedTasks().annotate(<q-args>)"
execute "command! -buffer -range -nargs=* -complete=customlist,taskwiki#CompleteMod TaskWikiMod :<line1>,<line2>" . g:taskwiki_py . "SelectedTasks().modify(<q-args>)"
" Interactive commands
execute "command! -buffer -range TaskWikiChooseProject :<line1>,<line2>" . g:taskwiki_py . "ChooseSplitProjects('global').execute()"

View file

@ -121,6 +121,7 @@ class TaskCache(object):
# Initialize all the subcomponents
self.buffer = BufferProxy(buffer_number)
self.completion = store.CompletionStore(self)
self.task = store.TaskStore(self)
self.presets = store.PresetStore(self)
self.vwtask = store.VwtaskStore(self)
@ -146,6 +147,7 @@ class TaskCache(object):
def reset(self):
self.buffer.obtain()
self.completion.store = dict()
self.task.store = dict()
self.vwtask.store = dict()
self.viewport.store = dict()
@ -307,3 +309,6 @@ class TaskCache(object):
from taskwiki import vwtask
task = vwtask.VimwikiTask.find_closest(self)
return task.tw if task else self.warriors['default']
def get_relevant_completion(self):
return self.completion[self.get_relevant_tw()]

115
taskwiki/completion.py Normal file
View file

@ -0,0 +1,115 @@
from functools import reduce, wraps
import re
from taskwiki import constants
def complete_last_word(f):
@wraps(f)
def wrapper(self, arglead):
before, sep, after = arglead.rpartition(' ')
comps = f(self, after)
if comps:
return [before + sep + comp for comp in comps]
else:
return []
return wrapper
# TODO(2023-06-27): use functools once python 3.7 is EOL
def cached_property(f):
@wraps(f)
def wrapper(self):
k = '_cache_' + f.__name__
if k in self.__dict__:
return self.__dict__[k]
else:
v = f(self)
self.__dict__[k] = v
return v
return wrapper
# "must*opt" -> "must(o(p(t)?)?)?"
def prefix_regex(s):
must, _, opt = s.partition('*')
return must + reduce(lambda y, x: f"({x}{y})?", reversed(opt), '')
RE_PROJECT = re.compile(prefix_regex('pro*ject'))
RE_DATE = re.compile('|'.join(
[prefix_regex(r)
for r in "du*e un*til wa*it ent*ry end st*art sc*heduled".split()]))
RE_RECUR = re.compile(prefix_regex('re*cur'))
class Completion():
def __init__(self, tw):
self.tw = tw
@cached_property
def _attributes(self):
return sorted(self.tw.execute_command(['_columns']))
@cached_property
def _tags(self):
return sorted(set(
tag
for tags in self.tw.execute_command(['_unique', 'tag'])
for tag in tags.split(',')))
@cached_property
def _projects(self):
return sorted(self.tw.execute_command(['_unique', 'project']))
def _complete_any(self, w):
if w:
return []
return ['+', '-'] + [attr + ':' for attr in self._attributes()]
def _complete_attributes(self, w):
if not w.isalpha():
return []
return [attr + ':'
for attr in self._attributes()
if attr.startswith(w)]
def _complete_tags(self, w):
if not w or w[0] not in ['+', '-']:
return []
t = w[1:]
return [w[0] + tag
for tag in self._tags()
if tag.startswith(t)]
def _comp_words(self, w, pattern, words):
before, sep, after = w.partition(':')
if not sep or not re.fullmatch(pattern, before):
return []
return [before + sep + word
for word in words()
if word.startswith(after)]
def _complete_projects(self, w):
return self._comp_words(w, RE_PROJECT, self._projects)
def _complete_dates(self, w):
return self._comp_words(w, RE_DATE, lambda: constants.COMPLETION_DATE)
def _complete_recur(self, w):
return self._comp_words(w, RE_RECUR, lambda: constants.COMPLETION_RECUR)
@complete_last_word
def modify(self, w):
return \
self._complete_any(w) or \
self._complete_attributes(w) or \
self._complete_projects(w) or \
self._complete_tags(w) or \
self._complete_dates(w) or \
self._complete_recur(w) or \
[]

View file

@ -1,2 +1,28 @@
DEFAULT_VIEWPORT_VIRTUAL_TAGS = ("-DELETED", "-PARENT")
DEFAULT_SORT_ORDER = "status+,end+,due+,priority-,project+"
COMPLETION_DATE = """
now
yesterday today tomorrow
later someday
monday tuesday wednesday thursday friday saturday sunday
january february march april may june july
august september october november december
sopd sod sond eopd eod eond
sopw sow sonw eopw eow eonw
sopww soww sonww eopww eoww eonww
sopm som sonm eopm eom eonm
sopq soq sonq eopq eoq eonq
sopy soy sony eopy eoy eony
goodfriday easter eastermonday ascension pentecost
midsommar midsommarafton juhannus
""".split()
COMPLETION_RECUR = """
daily day weekdays weekly biweekly fortnight monthly
quarterly semiannual annual yearly biannual biyearly
""".split()

View file

@ -17,6 +17,7 @@ from taskwiki import sort
from taskwiki import util
from taskwiki import viewport
from taskwiki import decorators
from taskwiki import completion
cache = cache_module.CacheRegistry()
@ -177,7 +178,9 @@ class SelectedTasks(object):
# If no modstring was passed as argument, ask the user interactively
if not modstring:
with util.current_line_highlighted():
modstring = util.get_input("Enter modifications: ")
modstring = util.get_input(
"Enter modifications: ",
completion="customlist,taskwiki#CompleteMod")
# We might have two same tasks in the range, make sure we do not pass the
# same uuid twice

View file

@ -203,3 +203,9 @@ class LineStore(NoNoneStore):
self.cache.buffer[position1] = self.cache.buffer[position2]
self.cache.buffer[position2] = temp
class CompletionStore(NoNoneStore):
def get_method(self, key):
from taskwiki import completion
return completion.Completion(key)

View file

@ -95,8 +95,11 @@ def tw_args_to_kwargs(args):
return output
def get_input(prompt="Enter: ", allow_empty=False):
value = vim.eval('input("%s")' % prompt)
def get_input(prompt="Enter: ", allow_empty=False, completion=None):
if completion is not None:
value = vim.eval('input("%s", "", "%s")' % (prompt, completion))
else:
value = vim.eval('input("%s")' % prompt)
vim.command('redraw')
# Check for empty value and bail out if not allowed

75
tests/test_completion.py Normal file
View file

@ -0,0 +1,75 @@
# -*- coding: utf-8 -*-
from taskwiki.completion import Completion
from tests.base import IntegrationTest
class FakeTW():
def __init__(self, projects=[], tags=[]):
self.projects = projects
self.tags = tags
def execute_command(self, args):
if args == ["_unique", "project"]:
return self.projects
elif args == ["_unique", "tag"]:
return self.tags
elif args == ["_columns"]:
return ["priority", "project", "due", "end"]
else:
assert False
class TestCompletionUnit():
def test_attributes(self):
c = Completion(FakeTW())
assert c.modify("") == ["+", "-", "due:", "end:", "priority:", "project:"]
assert c.modify("pr") == ["priority:", "project:"]
assert c.modify("pri") == ["priority:"]
def test_projects(self):
c = Completion(FakeTW(projects=["aa", "ab", "c"]))
assert c.modify("proj:") == ["proj:aa", "proj:ab", "proj:c"]
assert c.modify("proj:a") == ["proj:aa", "proj:ab"]
assert c.modify("proj:ab") == ["proj:ab"]
assert c.modify("proj:abc") == []
assert c.modify("pr:") == []
assert c.modify("project:ab") == ["project:ab"]
def test_tags(self):
c = Completion(FakeTW(tags=["aa", "aa,ab", "c"]))
assert c.modify("+") == ["+aa", "+ab", "+c"]
assert c.modify("+a") == ["+aa", "+ab"]
assert c.modify("+ab") == ["+ab"]
assert c.modify("+abc") == []
def test_dates(self):
c = Completion(FakeTW())
assert c.modify("end:no") == ["end:now", "end:november"]
assert c.modify("sch:jan") == ["sch:january"]
def test_recur(self):
c = Completion(FakeTW())
assert c.modify("re:da") == ["re:daily", "re:day"]
assert c.modify("recur:q") == ["recur:quarterly"]
class TestCompletionIntegration(IntegrationTest):
viminput = """
* [ ] test task 1 #{uuid}
* [ ] test task 2 #{uuid}
"""
tasks = [
dict(description="test task 1", project="ABC"),
dict(description="test task 2", project="DEF"),
]
def execute(self):
self.client.feedkeys(":TaskWikiMod\\<Enter>")
self.client.eval('feedkeys("pro\\<Tab>D\\<Tab>\\<Enter>", "t")')
self.client.eval('0') # wait for command completion
for task in self.tasks:
task.refresh()
assert self.tasks[0]['project'] == "DEF"