Add new header type "Preset Header"

As first envisioned in #69, these preset headers allow us to have a
hierarchical structure of taskwarrior filters and defaults.
This commit is contained in:
Bodo Graumann 2017-07-31 08:54:35 +02:00 committed by Tomas Babej
parent 489d00f24e
commit 4372d52298
6 changed files with 175 additions and 5 deletions

View file

@ -2,6 +2,7 @@ import vim # pylint: disable=F0401
import re
import six
from taskwiki import preset
from taskwiki import viewport
from taskwiki import regexp
from taskwiki import store
@ -115,6 +116,7 @@ class TaskCache(object):
self.buffer = BufferProxy(buffer_number)
self.task = store.TaskStore(self)
self.presets = store.PresetStore(self)
self.vwtask = store.VwtaskStore(self)
self.viewport = store.ViewportStore(self)
self.line = store.LineStore(self)
@ -143,6 +145,23 @@ class TaskCache(object):
self.viewport.store = dict()
self.line.store = dict()
def load_presets(self):
stack = []
for i in range(len(self.buffer)):
header = preset.PresetHeader.from_line(i, self, stack[-1] if stack else None)
if header is None:
continue
# Save the preset header in the cache
self.presets[i] = header
while stack and stack[-1].level >= header.level:
stack.pop()
stack.append(header)
def load_vwtasks(self, buffer_has_authority=True):
# Set the authority flag, which determines which data (Buffer or TW)
# will be considered authoritative

View file

@ -35,6 +35,7 @@ class WholeBuffer(object):
c = cache()
c.reset()
c.load_tasks()
c.load_presets()
c.load_vwtasks(buffer_has_authority=False)
c.load_viewports()
c.update_vwtasks_from_tasks()
@ -53,6 +54,7 @@ class WholeBuffer(object):
c = cache()
c.reset()
c.load_tasks()
c.load_presets()
c.load_vwtasks()
c.load_viewports()
c.save_tasks()

96
taskwiki/preset.py Normal file
View file

@ -0,0 +1,96 @@
import re
import six
from taskwiki import regexp
from taskwiki import util
class PresetHeader(object):
"""
== Taskwiki tasks || project:taskwiki ==
"""
def __init__(self, cache, parent, level, filterstring, defaultstring):
self.level = level
self.parent = parent
if parent:
taskfilter = list(parent.taskfilter)
else:
taskfilter = []
if filterstring:
taskfilter += '('
taskfilter += util.tw_modstring_to_args(filterstring)
taskfilter += ')'
self.taskfilter = taskfilter
if parent:
defaults = dict(parent.defaults)
else:
defaults = dict()
if defaultstring:
defaults.update(util.tw_modstring_to_kwargs(defaultstring))
else:
defaults.update(util.tw_args_to_kwargs(taskfilter))
self.defaults = defaults
@classmethod
def parse_line(cls, cache, number):
header = re.search(regexp.GENERIC_HEADER, cache.buffer[number])
if header:
preset = re.search(regexp.GENERIC_PRESET, cache.buffer[number])
if preset:
return preset
return header
@classmethod
def from_line(cls, number, cache, previous=None):
match = cache.line[(cls, number)]
if not match:
return None
level = len(match.group('header_start'))
if level == 1:
parent = None
else:
# Manually get previous header
if not previous:
for i in reversed(range(0, number)):
previous = cls.from_line(i, cache)
if previous:
break
# find parent
parent = previous
while parent and parent.level >= level:
parent = parent.parent
if parent is None:
# Create an empty root stub
parent = cls(cache, None, 0, None, None)
# use parent object, if no additional filters / defaults are applied
try:
filterstring = match.group('filter')
except IndexError:
return parent
defaults = match.group('defaults')
if six.PY2:
filterstring = filterstring.decode('utf-8')
defaults = defaults.decode('utf-8') if defaults is not None else defaults
self = cls(cache, parent, level, filterstring, defaults)
return self

View file

@ -60,12 +60,27 @@ GENERIC_VIEWPORT = re.compile(
'[=]+' # Header ending
)
GENERIC_PRESET = re.compile(
'^' # Starts at the beginning of the line
'(?P<header_start>[=]+)' # With a positive number of =
'([^=\|\[\{]*)' # Heading caption, everything up to ||
# Cannot include '[', '=', '|, and '{'
'\|\|' # Delimiter
'(?P<filter>[^=\|]+?)' # Filter preset
'(' # Optional defaults
'\|\|' # Delimiter
'(?P<defaults>[^=\|]+?)' # Default attrs preset
')?'
'\s*' # Any whitespace
'[=]+' # Header ending
)
GENERIC_HEADER = re.compile(
'^' # Starts at the beginning of the line
'[=]+' # With a positive number of =
'[^=]+' # Character other than =
'[=]+' # Positive number of =, closing the header
'\s*' # Allow trailing whitespace
'^' # Starts at the beginning of the line
'(?P<header_start>[=]+)' # With a positive number of =
'[^=]+' # Character other than =
'[=]+' # Positive number of =, closing the header
'\s*' # Allow trailing whitespace
)
ANSI_ESCAPE_SEQ = re.compile(

View file

@ -149,6 +149,13 @@ class ViewportStore(LineNumberedKeyedStoreMixin, NoNoneStore):
return viewport.ViewPort.from_line(line, self.cache)
class PresetStore(LineNumberedKeyedStoreMixin, NoNoneStore):
def get_method(self, line):
import preset
return preset.PresetHeader.from_line(line, self.cache)
class LineStore(NoNoneStore):
def __delitem__(self, number):

View file

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
from tests.base import MockVim, MockCache
import sys
class TestParsingPresetHeader(object):
def setup(self):
self.mockvim = MockVim()
self.cache = MockCache()
sys.modules['vim'] = self.mockvim
from taskwiki.preset import PresetHeader
self.PresetHeader = PresetHeader
def teardown(self):
self.mockvim.reset()
self.cache.reset()
def test_simple(self):
self.cache.buffer[0] = "== Test || project:Home =="
header = self.PresetHeader.from_line(0, self.cache)
assert header.taskfilter == ["(", "project:Home", ")"]
assert header.defaults == {'project': 'Home'}
def test_defaults(self):
self.cache.buffer[0] = "== Test || project:Home || +home =="
header = self.PresetHeader.from_line(0, self.cache)
assert header.taskfilter == ["(", "project:Home", ")"]
assert header.defaults == {'tags': ['home']}