taskwiki/autoload/vimwiki-pytasks.py
2014-12-31 05:34:12 +01:00

218 lines
6.5 KiB
Python

import vim
import re
from tasklib.task import TaskWarrior, Task
# Unnamed building blocks
UUID_UNNAMED = r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'
SPACE_UNNAMED = r'\s*'
NONEMPTY_SPACE_UNNAMED = r'\s+'
FINAL_SEGMENT_SEPARATOR_UNNAMED = r'(\s+|$)'
# Building blocks
BRACKET_OPENING = re.escape('* [')
BRACKET_CLOSING = re.escape('] ')
EMPTY_SPACE = r'(?P<space>\s*)'
UUID = r'(?P<uuid>{0})'.format(UUID_UNNAMED)
TEXT = r'(?P<text>.+(?<!{0})(?<!\s))'.format(UUID_UNNAMED)
DUE = r'(?P<due>\(\d{4}-\d\d-\d\d( \d\d:\d\d)?\))'
COMPLETION_MARK = r'(?P<completed>.)'
UUID_COMMENT = '#{0}'.format(UUID)
# Middle building blocks
INCOMPLETE_TASK_PREFIX = EMPTY_SPACE + BRACKET_OPENING + '[^X]' + BRACKET_CLOSING + TEXT
# Final regexps
TASKS_TO_SAVE_TO_TW = ''.join([
INCOMPLETE_TASK_PREFIX, # any amount of whitespace followed by uncompleted square
FINAL_SEGMENT_SEPARATOR_UNNAMED,
'(', DUE, FINAL_SEGMENT_SEPARATOR_UNNAMED, ')?' # Due is optional
'(', UUID_COMMENT, FINAL_SEGMENT_SEPARATOR_UNNAMED, ')?' # UUID is not there for new tasks
])
GENERIC_TASK = re.compile(''.join([
EMPTY_SPACE,
BRACKET_OPENING,
COMPLETION_MARK,
BRACKET_CLOSING,
TEXT,
FINAL_SEGMENT_SEPARATOR_UNNAMED,
'(', DUE, FINAL_SEGMENT_SEPARATOR_UNNAMED, ')?' # Due is optional
'(', UUID_COMMENT, FINAL_SEGMENT_SEPARATOR_UNNAMED, ')?' # UUID is not there for new tasks
]))
"""
How this plugin works:
1.) On startup, it reads all the tasks and syncs info TW -> Vimwiki file. Task is identified by their
uuid.
2.) When saving, the opposite sync is performed (Vimwiki -> TW direction).
a) if task is marked as subtask by indentation, the dependency is created between
"""
tw = TaskWarrior()
class TaskCache(object):
def __init__(self):
self.cache = dict()
def __getitem__(self, key):
task = self.cache.get(key)
if task is None:
task = VimwikiTask(vim.current.buffer[key], key)
self.cache[key] = task
return task
def __iter__(self):
while self.cache.keys():
for key in list(self.cache.keys()):
task = self.cache[key]
if all([t.line_number not in self.cache.keys()
for t in task.add_dependencies]):
del self.cache[key]
yield task
def reset(self):
self.cache = dict()
cache = TaskCache()
class VimwikiTask(object):
def __init__(self, line, position):
"""
Constructs a Vimwiki task from line at given position at the buffer
"""
match = re.search(GENERIC_TASK, line)
self.indent = match.group('space')
self.text = match.group('text')
self.uuid = match.group('uuid') # can be None for new tasks
self.due = match.group('due') # TODO: convert to proper timestamp
self.completed_mark = match.group('completed')
self.completed = self.completed_mark is 'X'
self.line_number = position
# We need to track depedency set in a extra attribute, since
# this may be a new task, and hence it need not to be saved yet.
# We circumvent this problem by iteration order in the TaskCache
self.add_dependencies = set()
# First set the task attribute to None, then try to load it, if possible
self.task = None
if self.uuid:
try:
self.task = tw.tasks.get(uuid=self.uuid)
except Task.DoesNotExist:
self.task = Task(tw)
# If task cannot be loaded, we need to remove the UUID
vim.command('echom UUID not found:"%s",'
'will be replaced if saved' % self.uuid)
self.uuid = None
else:
self.task = Task(tw)
self.parent = self.find_parent_task()
# Make parent task dependant on this task
if self.parent:
self.parent.add_dependencies |= set([self])
def save_to_tw(self):
# Push the values to the Task only if the Vimwiki representation
# somehow differs
# TODO: Check more than description
if self.task['description'] != self.text:
self.task['description'] = self.text
self.task['depends'] |= set(s.task for s in self.add_dependencies
if not s.task.completed)
self.task.save()
# Load the UUID
if not self.uuid:
self.uuid = self.task['uuid']
# Mark task as done. This works fine with already completed tasks.
if self.completed and (self.task.pending or self.task.waiting):
self.task.done()
def update_from_tw(self, refresh=False):
if not self.task:
return
# We refresh only if specified, since sometimes we
# know that the TW task is up to date (e.g. because we just
# loaded the object)
if refresh:
self.task.refresh()
self.text = self.task['description']
# TODO: update due
self.completed = (self.task['status'] == u'completed')
def update_in_buffer(self):
vim.current.buffer[self.line_number] = str(self)
def __str__(self):
return ''.join([
self.indent,
'* [',
'X' if self.completed else self.completed_mark,
'] ',
self.text,
' #',
self.uuid or 'TW-NOT_SYNCED'
])
def find_parent_task(self):
for i in reversed(range(0, self.line_number)):
if re.search(GENERIC_TASK, vim.current.buffer[i]):
task = cache[i]
if len(task.indent) < len(self.indent):
return task
def update_from_tw():
"""
Updates all the incomplete tasks in the vimwiki file if the info from TW is different.
"""
for i in range(len(vim.current.buffer)):
line = vim.current.buffer[i]
if re.search(GENERIC_TASK, line):
task = cache[i]
task.update_from_tw()
task.update_in_buffer()
def update_to_tw():
"""
Updates all tasks that differ from their TaskWarrior representation.
"""
cache.reset()
for i in range(len(vim.current.buffer)):
line = vim.current.buffer[i]
# First load all the tasks to the cache (this will set dependency sets)
if re.search(GENERIC_TASK, line):
task = cache[i]
for task in cache:
task.save_to_tw()
task.update_in_buffer()
if __name__ == '__main__':
update_from_tw()