mirror of
https://github.com/tbabej/taskwiki.git
synced 2025-08-23 20:36:40 +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.'] '
|
let len_text = ' ['.fold_len.'] '
|
||||||
return short_text.len_text.repeat(' ', 500)
|
return short_text.len_text.repeat(' ', 500)
|
||||||
endfunction
|
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]
|
*:TaskWikiMod* [mods]
|
||||||
Opens a prompt for task modification, for selected task(s).
|
Opens a prompt for task modification, for selected task(s).
|
||||||
|
|
||||||
|
Supports |cmdline-completion|.
|
||||||
|
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
Interactive commands.
|
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 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=* 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=* 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
|
" Interactive commands
|
||||||
execute "command! -buffer -range TaskWikiChooseProject :<line1>,<line2>" . g:taskwiki_py . "ChooseSplitProjects('global').execute()"
|
execute "command! -buffer -range TaskWikiChooseProject :<line1>,<line2>" . g:taskwiki_py . "ChooseSplitProjects('global').execute()"
|
||||||
|
|
|
@ -121,6 +121,7 @@ class TaskCache(object):
|
||||||
|
|
||||||
# Initialize all the subcomponents
|
# Initialize all the subcomponents
|
||||||
self.buffer = BufferProxy(buffer_number)
|
self.buffer = BufferProxy(buffer_number)
|
||||||
|
self.completion = store.CompletionStore(self)
|
||||||
self.task = store.TaskStore(self)
|
self.task = store.TaskStore(self)
|
||||||
self.presets = store.PresetStore(self)
|
self.presets = store.PresetStore(self)
|
||||||
self.vwtask = store.VwtaskStore(self)
|
self.vwtask = store.VwtaskStore(self)
|
||||||
|
@ -146,6 +147,7 @@ class TaskCache(object):
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.buffer.obtain()
|
self.buffer.obtain()
|
||||||
|
self.completion.store = dict()
|
||||||
self.task.store = dict()
|
self.task.store = dict()
|
||||||
self.vwtask.store = dict()
|
self.vwtask.store = dict()
|
||||||
self.viewport.store = dict()
|
self.viewport.store = dict()
|
||||||
|
@ -307,3 +309,6 @@ class TaskCache(object):
|
||||||
from taskwiki import vwtask
|
from taskwiki import vwtask
|
||||||
task = vwtask.VimwikiTask.find_closest(self)
|
task = vwtask.VimwikiTask.find_closest(self)
|
||||||
return task.tw if task else self.warriors['default']
|
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_VIEWPORT_VIRTUAL_TAGS = ("-DELETED", "-PARENT")
|
||||||
DEFAULT_SORT_ORDER = "status+,end+,due+,priority-,project+"
|
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 util
|
||||||
from taskwiki import viewport
|
from taskwiki import viewport
|
||||||
from taskwiki import decorators
|
from taskwiki import decorators
|
||||||
|
from taskwiki import completion
|
||||||
|
|
||||||
|
|
||||||
cache = cache_module.CacheRegistry()
|
cache = cache_module.CacheRegistry()
|
||||||
|
@ -177,7 +178,9 @@ class SelectedTasks(object):
|
||||||
# If no modstring was passed as argument, ask the user interactively
|
# If no modstring was passed as argument, ask the user interactively
|
||||||
if not modstring:
|
if not modstring:
|
||||||
with util.current_line_highlighted():
|
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
|
# We might have two same tasks in the range, make sure we do not pass the
|
||||||
# same uuid twice
|
# same uuid twice
|
||||||
|
|
|
@ -203,3 +203,9 @@ class LineStore(NoNoneStore):
|
||||||
self.cache.buffer[position1] = self.cache.buffer[position2]
|
self.cache.buffer[position1] = self.cache.buffer[position2]
|
||||||
self.cache.buffer[position2] = temp
|
self.cache.buffer[position2] = temp
|
||||||
|
|
||||||
|
|
||||||
|
class CompletionStore(NoNoneStore):
|
||||||
|
|
||||||
|
def get_method(self, key):
|
||||||
|
from taskwiki import completion
|
||||||
|
return completion.Completion(key)
|
||||||
|
|
|
@ -95,7 +95,10 @@ def tw_args_to_kwargs(args):
|
||||||
|
|
||||||
return output
|
return output
|
||||||
|
|
||||||
def get_input(prompt="Enter: ", allow_empty=False):
|
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)
|
value = vim.eval('input("%s")' % prompt)
|
||||||
vim.command('redraw')
|
vim.command('redraw')
|
||||||
|
|
||||||
|
|
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