mirror of
https://github.com/tbabej/taskwiki.git
synced 2025-08-23 02:23:07 +02:00
completion: Add tab completion for TaskWikiMod
This commit is contained in:
parent
a6907bac85
commit
6cedc13b58
10 changed files with 243 additions and 4 deletions
|
@ -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
|
||||
|
|
|
@ -604,6 +604,8 @@ selected tasks.
|
|||
*:TaskWikiMod* [mods]
|
||||
Opens a prompt for task modification, for selected task(s).
|
||||
|
||||
Supports |cmdline-completion|.
|
||||
|
||||
----------------------------------------------------------------------------
|
||||
Interactive commands.
|
||||
|
||||
|
|
|
@ -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()"
|
||||
|
|
|
@ -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
115
taskwiki/completion.py
Normal 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 \
|
||||
[]
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
75
tests/test_completion.py
Normal 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"
|
Loading…
Add table
Add a link
Reference in a new issue