diff --git a/taskwiki/cache.py b/taskwiki/cache.py index ec73b9e..5acd19e 100644 --- a/taskwiki/cache.py +++ b/taskwiki/cache.py @@ -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 diff --git a/taskwiki/main.py b/taskwiki/main.py index f791a51..b55746d 100644 --- a/taskwiki/main.py +++ b/taskwiki/main.py @@ -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() diff --git a/taskwiki/preset.py b/taskwiki/preset.py new file mode 100644 index 0000000..2db877e --- /dev/null +++ b/taskwiki/preset.py @@ -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 diff --git a/taskwiki/regexp.py b/taskwiki/regexp.py index ccef0f4..89e21e8 100644 --- a/taskwiki/regexp.py +++ b/taskwiki/regexp.py @@ -60,12 +60,27 @@ GENERIC_VIEWPORT = re.compile( '[=]+' # Header ending ) +GENERIC_PRESET = re.compile( + '^' # Starts at the beginning of the line + '(?P[=]+)' # With a positive number of = + '([^=\|\[\{]*)' # Heading caption, everything up to || + # Cannot include '[', '=', '|, and '{' + '\|\|' # Delimiter + '(?P[^=\|]+?)' # Filter preset + '(' # Optional defaults + '\|\|' # Delimiter + '(?P[^=\|]+?)' # 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[=]+)' # With a positive number of = + '[^=]+' # Character other than = + '[=]+' # Positive number of =, closing the header + '\s*' # Allow trailing whitespace ) ANSI_ESCAPE_SEQ = re.compile( diff --git a/taskwiki/store.py b/taskwiki/store.py index e59dab1..4e3dcd4 100644 --- a/taskwiki/store.py +++ b/taskwiki/store.py @@ -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): diff --git a/tests/test_preset_parsing.py b/tests/test_preset_parsing.py new file mode 100644 index 0000000..76a8a4d --- /dev/null +++ b/tests/test_preset_parsing.py @@ -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']}