diff --git a/doc/orgguide.txt b/doc/orgguide.txt index e8fe382d..35e93db6 100644 --- a/doc/orgguide.txt +++ b/doc/orgguide.txt @@ -168,8 +168,13 @@ via the 'Org' menu. Most are only usable in command mode. Dates:~ sa - insert date si - insert inactive date + ssc - set the SCHEDULED date of the current headline + sdl - set the DEADLINE date of the current headline pa - insert date by using calendar selection pi - insert inactive date by using calendar selection + psc - set the SCHEDULED date by using calendar selection + pdl - set the DEADLINE date by using calendar selection + Agenda:~ caa - agenda for the week diff --git a/ftplugin/org.vim b/ftplugin/org.vim index f973ab63..af6ed291 100644 --- a/ftplugin/org.vim +++ b/ftplugin/org.vim @@ -116,6 +116,7 @@ for p in vim.eval("&runtimepath").split(','): break from orgmode._vim import ORGMODE, insert_at_cursor, get_user_input, date_to_str +from orgmode._vim import ORGMODE, set_scheduled_date, set_deadline_date ORGMODE.start() from Date import Date @@ -167,3 +168,37 @@ fun CalendarAction(day, month, year, week, dir) " restore calendar_action let g:calendar_action = g:org_calendar_action_backup endf +fun CalendarActionScheduled(day, month, year, week, dir) + let g:org_timestamp = printf("%04d-%02d-%02d Fri", a:year, a:month, a:day) + let datetime_date = printf("datetime.date(%d, %d, %d)", a:year, a:month, a:day) + exe s:py_version . "selected_date = " . datetime_date + " get_user_input + let msg = printf("Inserting %s | Modify date", g:org_timestamp) + exe s:py_version . "modifier = get_user_input('" . msg . "')" + " change date according to user input + exe s:py_version . "newdate = Date._modify_time(selected_date, modifier)" + " close Calendar + exe "q" + " goto previous window + exe "wincmd p" + exe s:py_version . "set_scheduled_date(newdate)" + " restore calendar_action + let g:calendar_action = g:org_calendar_action_backup +endf +fun CalendarActionDeadline(day, month, year, week, dir) + let g:org_timestamp = printf("%04d-%02d-%02d Fri", a:year, a:month, a:day) + let datetime_date = printf("datetime.date(%d, %d, %d)", a:year, a:month, a:day) + exe s:py_version . "selected_date = " . datetime_date + " get_user_input + let msg = printf("Inserting %s | Modify date", g:org_timestamp) + exe s:py_version . "modifier = get_user_input('" . msg . "')" + " change date according to user input + exe s:py_version . "newdate = Date._modify_time(selected_date, modifier)" + " close Calendar + exe "q" + " goto previous window + exe "wincmd p" + exe s:py_version . "set_deadline_date(newdate)" + " restore calendar_action + let g:calendar_action = g:org_calendar_action_backup +endf diff --git a/ftplugin/orgmode/_vim.py b/ftplugin/orgmode/_vim.py index 73ba2c4a..5d4b6adb 100644 --- a/ftplugin/orgmode/_vim.py +++ b/ftplugin/orgmode/_vim.py @@ -21,7 +21,7 @@ from orgmode.exceptions import PluginError from orgmode.vimbuffer import VimBuffer from orgmode.liborgmode.agenda import AgendaManager - +from orgmode.liborgmode.orgdate import get_orgdate REPEAT_EXISTS = bool(int(vim.eval('exists("*repeat#set()")'))) TAGSPROPERTIES_EXISTS = False @@ -165,6 +165,39 @@ def get_bufname(bufnr): if b.number == bufnr: return b.name +def get_heading(allow_dirty=False, line=None): + if not line: + line = int(vim.eval(u_encode(u'v:lnum'))) + d = ORGMODE.get_document(allow_dirty=allow_dirty) + heading = None + if allow_dirty: + heading = d.find_current_heading(line - 1) + else: + heading = d.current_heading(line - 1) + return d, heading + +def set_scheduled_date(new_date): + u""" Set the SCHEDULED entry in the Planning line of the current heading + + """ + allow_dirty = True + line, col = vim.current.window.cursor + doc, heading = get_heading(allow_dirty=allow_dirty, line=line) + new_date = get_orgdate(u"<%s>" % date_to_str(new_date)) + heading.scheduled_date = new_date + doc.write_heading(heading) + + +def set_deadline_date(new_date): + u""" Set de DEADLINE entry in the Planning line of the current heading + """ + allow_dirty = True + line, col = vim.current.window.cursor + doc, heading = get_heading(allow_dirty=allow_dirty, line=line) + new_date = get_orgdate("<%s>" % date_to_str(new_date)) + heading.deadline_date = new_date + doc.write_heading(heading) + def indent_orgmode(): u""" Set the indent value for the current line in the variable @@ -176,8 +209,7 @@ def indent_orgmode(): :returns: None """ line = int(vim.eval(u_encode(u'v:lnum'))) - d = ORGMODE.get_document() - heading = d.current_heading(line - 1) + doc, heading = get_heading() if heading and line != heading.start_vim: heading.init_checkboxes() checkbox = heading.current_checkbox() @@ -200,12 +232,7 @@ def fold_text(allow_dirty=False): :returns: None """ line = int(vim.eval(u_encode(u'v:foldstart'))) - d = ORGMODE.get_document(allow_dirty=allow_dirty) - heading = None - if allow_dirty: - heading = d.find_current_heading(line - 1) - else: - heading = d.current_heading(line - 1) + doc, heading = get_heading(allow_dirty, line) if heading: str_heading = unicode(heading) @@ -234,12 +261,7 @@ def fold_orgmode(allow_dirty=False): :returns: None """ line = int(vim.eval(u_encode(u'v:lnum'))) - d = ORGMODE.get_document(allow_dirty=allow_dirty) - heading = None - if allow_dirty: - heading = d.find_current_heading(line - 1) - else: - heading = d.current_heading(line - 1) + doc, heading = get_heading(allow_dirty) # if cache_heading != heading: # heading.init_checkboxes() diff --git a/ftplugin/orgmode/liborgmode/documents.py b/ftplugin/orgmode/liborgmode/documents.py index a2eeca76..a5d016fd 100644 --- a/ftplugin/orgmode/liborgmode/documents.py +++ b/ftplugin/orgmode/liborgmode/documents.py @@ -12,12 +12,17 @@ except: from UserList import UserList +from orgmode import settings + from orgmode.liborgmode.base import MultiPurposeList, flatten_list, Direction, get_domobj_range from orgmode.liborgmode.headings import Heading, HeadingList from orgmode.py3compat.encode_compatibility import * from orgmode.py3compat.unicode_compatibility import * +import re +REGEX_LOGGING_MODIFIERS = re.compile(r"[!@/]") + class Document(object): u""" Representation of a whole org-mode document. @@ -51,7 +56,8 @@ def __init__(self): self._tag_column = 77 # TODO this doesn't differentiate between ACTIVE and FINISHED todo's - self.todo_states = [u'TODO', u'DONE'] + self.todo_states_stripped = self.get_settings_todo_states(True) + self.todo_states = self.get_settings_todo_states(False) def __unicode__(self): if self.meta_information is None: @@ -61,6 +67,65 @@ def __unicode__(self): def __str__(self): return u_encode(self.__unicode__()) + def get_done_states(self, strip_access_key=True): + all_states = self.get_todo_states(strip_access_key) + done_states = list([ done_state for x in all_states for done_state in x[1]]) + + return done_states + + def parse_todo_settings(self, setting, strip_access_key = True): + def parse_states(s, stop=0): + res = [] + if not s: + return res + if type(s[0]) in (unicode, str): + r = [] + for i in s: + _i = i + if type(_i) == str: + _i = u_decode(_i) + if type(_i) == unicode and _i: + if strip_access_key and u'(' in _i: + _i = _i[:_i.index(u'(')] + if _i: + r.append(_i) + else: + _i = REGEX_LOGGING_MODIFIERS.sub("", _i) + r.append(_i) + if not u'|' in r: + if not stop: + res.append((r[:-1], [r[-1]])) + else: + res = (r[:-1], [r[-1]]) + else: + seperator_pos = r.index(u'|') + if not stop: + res.append((r[0:seperator_pos], r[seperator_pos + 1:])) + else: + res = (r[0:seperator_pos], r[seperator_pos + 1:]) + elif type(s) in (list, tuple) and not stop: + for i in s: + r = parse_states(i, stop=1) + if r: + res.append(r) + return res + return parse_states(setting) + + + def get_settings_todo_states(self, strip_access_key=True): + u""" Returns a list containing a tuple of two lists of allowed todo + states split by todo and done states. Multiple todo-done state + sequences can be defined. + + :returns: [([todo states], [done states]), ..] + """ + states = settings.get(u'org_todo_keywords', []) + + if type(states) not in (list, tuple): + return [] + + return self.parse_todo_settings(states, strip_access_key) + def get_all_todo_states(self): u""" Convenience function that returns all todo and done states and sequences in one big list. @@ -71,7 +136,7 @@ def get_all_todo_states(self): # TODO This is not necessary remove return flatten_list(self.get_todo_states()) - def get_todo_states(self): + def get_todo_states(self, strip_access_key=True): u""" Returns a list containing a tuple of two lists of allowed todo states split by todo and done states. Multiple todo-done state sequences can be defined. @@ -82,7 +147,12 @@ def get_todo_states(self): # TODO this should be made into property so todo states can be set like # this too.. or there was also some todo property around... oh well.. # TODO there is the same method in vimbuffer - return self.todo_states + + ret = self.todo_states + if strip_access_key: + ret = self.todo_states_stripped + + return ret @property def tabstop(self): diff --git a/ftplugin/orgmode/liborgmode/dom_obj.py b/ftplugin/orgmode/liborgmode/dom_obj.py index 5270d190..1a797d5d 100644 --- a/ftplugin/orgmode/liborgmode/dom_obj.py +++ b/ftplugin/orgmode/liborgmode/dom_obj.py @@ -31,6 +31,8 @@ flags=re.U) REGEX_TODO = re.compile(r'^[^\s]*$') +REGEX_PLANNING = re.compile(r'(CLOSED|SCHEDULED|DEADLINE)\s*:[^]>]+(\]|>)', flags=re.U) + # checkbox regex: # - [ ] checkbox item # - [X] checkbox item diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index 228a4ba1..ca4b36d3 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -14,7 +14,7 @@ from orgmode.liborgmode.orgdate import OrgTimeRange from orgmode.liborgmode.orgdate import get_orgdate from orgmode.liborgmode.checkboxes import Checkbox, CheckboxList -from orgmode.liborgmode.dom_obj import DomObj, DomObjList, REGEX_SUBTASK, REGEX_SUBTASK_PERCENT, REGEX_HEADING, REGEX_TAG, REGEX_TODO +from orgmode.liborgmode.dom_obj import DomObj, DomObjList, REGEX_SUBTASK, REGEX_SUBTASK_PERCENT, REGEX_HEADING, REGEX_TAG, REGEX_TODO, REGEX_PLANNING from orgmode.py3compat.xrange_compatibility import * from orgmode.py3compat.encode_compatibility import * @@ -29,14 +29,18 @@ class Heading(DomObj): u""" Structural heading object """ - def __init__(self, level=1, title=u'', tags=None, todo=None, body=None, active_date=None): + def __init__(self, level=1, title=u'', tags=None, todo=None, body=None, active_date=None, + scheduled_date=None, closed_date=None, deadline_date=None, has_planning_line=False): u""" - :level: Level of the heading - :title: Title of the heading - :tags: Tags of the heading - :todo: Todo state of the heading - :body: Body of the heading - :active_date: active date that is used in the agenda + :level: Level of the heading + :title: Title of the heading + :tags: Tags of the heading + :todo: Todo state of the heading + :body: Body of the heading + :active_date: active date that is used in the agenda + :scheduled_date: planning line SCHEDULED keyword datetime + :deadline_date: planning line DEADLINE keyword datetime + :closed_date: planning line CLOSED keyword datetime """ DomObj.__init__(self, level=level, title=title, body=body) @@ -55,8 +59,12 @@ def __init__(self, level=1, title=u'', tags=None, todo=None, body=None, active_d # active date self._active_date = active_date - if active_date: - self.active_date = active_date + + # planning dates + self._scheduled_date = scheduled_date + self._closed_date = closed_date + self._deadline_date = deadline_date + self.has_planning_line = has_planning_line # checkboxes self._checkboxes = CheckboxList(obj=self) @@ -153,39 +161,13 @@ def __ge__(self, other): """ Headings can be sorted by date. """ - try: - if self.active_date > other.active_date: - return True - elif self.active_date == other.active_date: - return True - elif self.active_date < other.active_date: - return False - except: - if not self.active_date and other.active_date: - return True - elif self.active_date and not other.active_date: - return False - elif not self.active_date and not other.active: - return True + return not self < other def __gt__(self, other): """ Headings can be sorted by date. """ - try: - if self.active_date > other.active_date: - return True - elif self.active_date == other.active_date: - return False - elif self.active_date < other.active_date: - return False - except: - if not self.active_date and other.active_date: - return True - elif self.active_date and not other.active_date: - return False - elif not self.active_date and not other.active: - return False + return not self <= other def copy(self, including_children=True, parent=None): u""" @@ -200,7 +182,10 @@ def copy(self, including_children=True, parent=None): """ heading = self.__class__( level=self.level, title=self.title, - tags=self.tags, todo=self.todo, body=self.body[:]) + tags=self.tags, todo=self.todo, body=self.body[:], + scheduled_date=self.scheduled_date, deadline_date=self.deadline_date, + closed_date=self.closed_date, active_date=self.active_date, + has_planning_line=self.has_planning_line) if parent: parent.children.append(heading) if including_children and self.children: @@ -390,6 +375,15 @@ def first_checkbox(self): if self.checkboxes: return self.checkboxes[0] + def parse_planning_line(self, planning_line): + KEYWORDS = self.get_plannings() + matches = list(REGEX_PLANNING.finditer(planning_line)) + + for m in matches: + if m.group(1) in KEYWORDS: + KEYWORDS[m.group(1)] = get_orgdate(str(m)) + return KEYWORDS + @classmethod def parse_heading_from_data( cls, data, allowed_todo_states, document=None, @@ -440,10 +434,17 @@ def parse_title(heading_line): if not data: raise ValueError(u'Unable to create heading, no data provided.') - # create new heaing + # create new heading new_heading = cls() new_heading.level, new_heading.todo, new_heading.title, new_heading.tags = parse_title(data[0]) new_heading.body = data[1:] + if new_heading.body: + new_heading._scheduled_date, new_heading._deadline_date, new_heading._closed_date =\ + new_heading.parse_planning_line(data[1]).values() + new_heading.has_planning_line = bool (new_heading.scheduled_date or \ + new_heading.deadline_date or \ + new_heading.closed_date or \ + data[1].strip() == "") if orig_start is not None: new_heading._dirty_heading = False new_heading._dirty_body = False @@ -601,6 +602,33 @@ def todo(self, value): def todo(self): self.todo = None + @property + def scheduled_date(self): + u""" + scheduled date + + supports PLANNING lines SCHEDULED keyword + """ + return self._scheduled_date + + @property + def closed_date(self): + u""" + scheduled date + + supports PLANNING lines SCHEDULED keyword + """ + return self._closed_date + + @property + def deadline_date(self): + u""" + scheduled date + + supports PLANNING lines SCHEDULED keyword + """ + return self._deadline_date + @property def active_date(self): u""" @@ -611,14 +639,81 @@ def active_date(self): """ return self._active_date + def render_planning_line(self, line=""): + matches = [] + if line: + matches = list(REGEX_PLANNING.finditer(line)) + plannings = self.get_plannings() + for k in plannings: + new_str = "" + if plannings[k]: + new_str = "%s: %s" % (k, plannings[k]) + old = None + for m in matches: + if m.group(1) == k: + old = m.group(0) + if old: + line = line.replace(old, new_str) + else: + if new_str: + line = line + " " + new_str + + return line + + def update_planning_line(self): + if self.has_planning_line: + self.body[0] = self.render_planning_line(self.body[0]) + else: + self.body.insert(0, self.render_planning_line()) + + @scheduled_date.setter + def scheduled_date(self, value): + if self.scheduled_date != value: + self._scheduled_date = value + self.update_planning_line() + + + @closed_date.setter + def closed_date(self, value): + if self.closed_date != value: + self._closed_date = value + self.update_planning_line() + + @deadline_date.setter + def deadline_date(self, value): + if self.deadline_date != value: + self._deadline_date = value + self.update_planning_line() + @active_date.setter def active_date(self, value): self._active_date = value + @scheduled_date.deleter + def scheduled_date(self): + self._scheduled_date = None + self.update_planning_line() + + @closed_date.deleter + def closed_date(self): + self._closed_date = None + self.update_planning_line() + + @deadline_date.deleter + def deadline_date(self): + self._deadline_date = None + self.update_planning_line() + @active_date.deleter def active_date(self): self._active_date = None + def get_plannings(self): + KEYWORDS = { "SCHEDULED" : self.scheduled_date, + "DEADLINE" : self.deadline_date, + "CLOSED" : self.closed_date } + return KEYWORDS + @DomObj.title.setter def title(self, value): u""" Set the title and mark the document and the heading dirty """ diff --git a/ftplugin/orgmode/liborgmode/orgdate.py b/ftplugin/orgmode/liborgmode/orgdate.py index 93a6776a..d9151766 100644 --- a/ftplugin/orgmode/liborgmode/orgdate.py +++ b/ftplugin/orgmode/liborgmode/orgdate.py @@ -26,33 +26,36 @@ from orgmode.py3compat.encode_compatibility import * +_DAYNAME = r"[^]>\s+0-9-]+" +_DATE = r"(\d\d\d\d)-(\d\d)-(\d\d)(?:\s+%s)?" % _DAYNAME + # <2011-09-12 Mon> -_DATE_REGEX = re.compile(r"<(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w>", re.UNICODE) +_DATE_REGEX = re.compile(r"<%s>" %_DATE, re.UNICODE) # [2011-09-12 Mon] -_DATE_PASSIVE_REGEX = re.compile(r"\[(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w\]", re.UNICODE) +_DATE_PASSIVE_REGEX = re.compile(r"\[%s]" % _DATE, re.UNICODE) # <2011-09-12 Mon 10:20> _DATETIME_REGEX = re.compile( - r"<(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w (\d{1,2}):(\d\d)>", re.UNICODE) + r"<%s (\d{1,2}):(\d\d)>" % _DATE, re.UNICODE) # [2011-09-12 Mon 10:20] _DATETIME_PASSIVE_REGEX = re.compile( - r"\[(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w (\d{1,2}):(\d\d)\]", re.UNICODE) + r"\[%s (\d{1,2}):(\d\d)\]" % _DATE, re.UNICODE) # <2011-09-12 Mon>--<2011-09-13 Tue> _DATERANGE_REGEX = re.compile( # <2011-09-12 Mon>-- - r"<(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w>--" + r"<%s>--" # <2011-09-13 Tue> - "<(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w>", re.UNICODE) + "<%s>" % (_DATE, _DATE), re.UNICODE) # <2011-09-12 Mon 10:00>--<2011-09-12 Mon 11:00> _DATETIMERANGE_REGEX = re.compile( # <2011-09-12 Mon 10:00>-- - r"<(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w (\d\d):(\d\d)>--" + r"<%s (\d\d):(\d\d)>--" # <2011-09-12 Mon 11:00> - "<(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w (\d\d):(\d\d)>", re.UNICODE) + "<%s (\d\d):(\d\d)>" % (_DATE, _DATE), re.UNICODE) # <2011-09-12 Mon 10:00--12:00> _DATETIMERANGE_SAME_DAY_REGEX = re.compile( - r"<(\d\d\d\d)-(\d\d)-(\d\d) [A-Z]\w\w (\d\d):(\d\d)-(\d\d):(\d\d)>", re.UNICODE) + r"<%s (\d\d):(\d\d)-(\d\d):(\d\d)>" % _DATE, re.UNICODE) def get_orgdate(data): diff --git a/ftplugin/orgmode/plugins/Date.py b/ftplugin/orgmode/plugins/Date.py index 04a675ee..0cf00f0d 100644 --- a/ftplugin/orgmode/plugins/Date.py +++ b/ftplugin/orgmode/plugins/Date.py @@ -7,6 +7,7 @@ import vim from orgmode._vim import ORGMODE, echom, insert_at_cursor, get_user_input +from orgmode._vim import set_deadline_date, set_scheduled_date from orgmode import settings from orgmode.keybinding import Keybinding, Plug from orgmode.menu import Submenu, ActionEntry, add_cmd_mapping_menu @@ -224,7 +225,7 @@ def _modify_time(cls, startdate, modifier): return startdate @classmethod - def insert_timestamp(cls, active=True): + def get_timestamp_cmdline(cls): u""" Insert a timestamp at the cursor position. @@ -244,6 +245,13 @@ def insert_timestamp(cls, active=True): newdate = cls._modify_time(today, modifier) + return newdate + + + @classmethod + def insert_timestamp(cls, active=True): + newdate = cls.get_timestamp_cmdline() + # format if isinstance(newdate, datetime): newdate = newdate.strftime( @@ -251,12 +259,23 @@ def insert_timestamp(cls, active=True): else: newdate = newdate.strftime( u_decode(u_encode(u'%Y-%m-%d %a'))) + timestamp = u'<%s>' % newdate if active else u'[%s]' % newdate insert_at_cursor(timestamp) @classmethod - def insert_timestamp_with_calendar(cls, active=True): + def set_scheduled_cmdline(cls): + newdate = cls.get_timestamp_cmdline() + set_scheduled_date(newdate) + + @classmethod + def set_deadline_cmdline(cls): + newdate = cls.get_timestamp_cmdline() + set_deadline_date(newdate) + + @classmethod + def get_timestamp_calendar(cls, ca_func_name): u""" Insert a timestamp at the cursor position. Show fancy calendar to pick the date from. @@ -270,12 +289,24 @@ def insert_timestamp_with_calendar(cls, active=True): # backup calendar_action calendar_action = vim.eval("g:calendar_action") vim.command("let g:org_calendar_action_backup = '" + calendar_action + "'") - vim.command("let g:calendar_action = 'CalendarAction'") + vim.command("let g:calendar_action = '" + ca_func_name + "'") + + @classmethod + def insert_timestamp_with_calendar(cls, active=True): + cls.get_timestamp_calendar("CalendarAction") timestamp_template = u'<%s>' if active else u'[%s]' # timestamp template vim.command("let g:org_timestamp_template = '" + timestamp_template + "'") + @classmethod + def set_scheduled_calendar(cls): + cls.get_timestamp_calendar("CalendarActionScheduled") + + @classmethod + def set_deadline_calendar(cls): + cls.get_timestamp_calendar("CalendarActionDeadline") + def register(self): u""" Registration of the plugin. @@ -289,6 +320,35 @@ def register(self): function=u'%s ORGMODE.plugins[u"Date"].insert_timestamp()' % VIM_PY_CALL, menu_desrc=u'Timest&' ) + add_cmd_mapping_menu( + self, + name=u'OrgDateSetScheduledTimestampCmdLine', + key_mapping=u'ssc', + function=u'%s ORGMODE.plugins[u"Date"].set_scheduled_cmdline()' % VIM_PY_CALL, + menu_desrc=u'Modify SCHEDULED Planning Entry' + ) + add_cmd_mapping_menu( + self, + name=u'OrgDateSetScheduledTimestampWithCalendar', + key_mapping=u'psc', + function=u'%s ORGMODE.plugins[u"Date"].set_scheduled_calendar()' % VIM_PY_CALL, + menu_desrc=u'Modify SCHEDULED Planning Entry with Calendar' + ) + add_cmd_mapping_menu( + self, + name=u'OrgDateSetDeadlineTimestampCmdLine', + key_mapping=u'sdl', + function=u'%s ORGMODE.plugins[u"Date"].set_deadline_cmdline()' % VIM_PY_CALL, + menu_desrc=u'Modify DEADLINE Planning Entry' + ) + add_cmd_mapping_menu( + self, + name=u'OrgDateSetDeadlineTimestampWithCalendar', + key_mapping=u'pdl', + function=u'%s ORGMODE.plugins[u"Date"].set_deadline_calendar()' % VIM_PY_CALL, + menu_desrc=u'Modify DEADLINE Planning Entry with Calendar' + ) + add_cmd_mapping_menu( self, name=u'OrgDateInsertTimestampInactiveCmdLine', diff --git a/ftplugin/orgmode/plugins/Todo.py b/ftplugin/orgmode/plugins/Todo.py index ad1a1a04..34b8d8d6 100644 --- a/ftplugin/orgmode/plugins/Todo.py +++ b/ftplugin/orgmode/plugins/Todo.py @@ -3,13 +3,17 @@ import vim import itertools as it +import datetime + from orgmode._vim import echom, ORGMODE, apply_count, repeat, realign_tags from orgmode import settings from orgmode.liborgmode.base import Direction +from orgmode.liborgmode.orgdate import OrgDateTime from orgmode.menu import Submenu, ActionEntry from orgmode.keybinding import Keybinding, Plug from orgmode.exceptions import PluginError + # temporary todo states for differnent orgmode buffers ORGTODOSTATES = {} @@ -144,6 +148,15 @@ def _get_next_state( if todo_iter[1] == current_state), -1) return flattened_todos[(ind + next_dir) % len(flattened_todos)] + @classmethod + def print_plannings(cls): + d = ORGMODE.get_document(allow_dirty=True) + heading = d.find_current_heading() + if not heading: + return + print("Planning: SCHEDULED=%s, DEADLINE=%s, CLOSED=%s" % (heading.scheduled_date, + heading.deadline_date, heading.closed_date)) + @classmethod @realign_tags @repeat @@ -219,11 +232,20 @@ def set_todo_state(cls, state): if not heading: return - + done_states = d.get_done_states(strip_access_key = True) current_state = heading.todo # set new headline heading.todo = state + + if (current_state in done_states) != (state in done_states): + if state in done_states: + n = datetime.datetime.now() + heading.closed_date = OrgDateTime(False, n.year, n.month, n.day, n.hour, + n.minute) + else: + heading.closed_date = None + d.write_heading(heading) # move cursor along with the inserted state only when current position @@ -270,7 +292,7 @@ def init_org_todo(cls): if all_states is None: vim.command(u_encode(u'bw')) - echom(u'No todo states avaiable for buffer %s' % vim.current.buffer.name) + echom(u'No todo states available for buffer %s' % vim.current.buffer.name) for idx, state in enumerate(all_states): pairs = [split_access_key(x, sub=u' ') for x in it.chain(*state)] @@ -313,6 +335,11 @@ def register(self): u'%s ORGMODE.plugins[u"Todo"].toggle_todo_state(interactive=True)' % VIM_PY_CALL))) self.menu + ActionEntry(u'&TODO/DONE/- (interactiv)', self.keybindings[-1]) + self.keybindings.append(Keybinding(u'z', Plug( + u'OrgTodoPrintPlannings', + u'%s ORGMODE.plugins[u"Todo"].print_plannings()' % VIM_PY_CALL))) + self.menu + ActionEntry(u'&PLANNING DEBUG', self.keybindings[-1]) + # add submenu submenu = self.menu + Submenu(u'Select &keyword') diff --git a/ftplugin/orgmode/vimbuffer.py b/ftplugin/orgmode/vimbuffer.py index b4760fb8..ab52fb92 100644 --- a/ftplugin/orgmode/vimbuffer.py +++ b/ftplugin/orgmode/vimbuffer.py @@ -18,6 +18,7 @@ is UTF-8. """ + try: from collections import UserList except: @@ -33,7 +34,6 @@ from orgmode.py3compat.encode_compatibility import * from orgmode.py3compat.unicode_compatibility import * - class VimBuffer(Document): def __init__(self, bufnr=0): u""" @@ -89,62 +89,6 @@ def changedtick(self): def changedtick(self, value): self._changedtick = value - def get_todo_states(self, strip_access_key=True): - u""" Returns a list containing a tuple of two lists of allowed todo - states split by todo and done states. Multiple todo-done state - sequences can be defined. - - :returns: [([todo states], [done states]), ..] - """ - states = settings.get(u'org_todo_keywords', []) - # TODO this function gets called too many times when change of state of - # one todo is triggered, check with: - # print(states) - # this should be changed by saving todo states into some var and only - # if new states are set hook should be called to register them again - # into a property - # TODO move this to documents.py, it is all tangled up like this, no - # structure... - if type(states) not in (list, tuple): - return [] - - def parse_states(s, stop=0): - res = [] - if not s: - return res - if type(s[0]) in (unicode, str): - r = [] - for i in s: - _i = i - if type(_i) == str: - _i = u_decode(_i) - if type(_i) == unicode and _i: - if strip_access_key and u'(' in _i: - _i = _i[:_i.index(u'(')] - if _i: - r.append(_i) - else: - r.append(_i) - if not u'|' in r: - if not stop: - res.append((r[:-1], [r[-1]])) - else: - res = (r[:-1], [r[-1]]) - else: - seperator_pos = r.index(u'|') - if not stop: - res.append((r[0:seperator_pos], r[seperator_pos + 1:])) - else: - res = (r[0:seperator_pos], r[seperator_pos + 1:]) - elif type(s) in (list, tuple) and not stop: - for i in s: - r = parse_states(i, stop=1) - if r: - res.append(r) - return res - - return parse_states(states) - def update_changedtick(self): if self.bufnr == vim.current.buffer.number: self._changedtick = int(vim.eval(u_encode(u'b:changedtick'))) diff --git a/tests/test_libheading.py b/tests/test_libheading.py index 335b8dc5..dbb6e76e 100644 --- a/tests/test_libheading.py +++ b/tests/test_libheading.py @@ -30,6 +30,10 @@ def setUp(self): self.h_no_date = Heading.parse_heading_from_data(tmp, self.allowed_todo_states) + tmp = ["* This heading has an incative date [2011-08-26 Fri]"] + self.h_no_date_2 = Heading.parse_heading_from_data(tmp, self.allowed_todo_states) + + def test_heading_parsing_no_date(self): """"" 'text' doesn't contain any valid date. @@ -38,15 +42,7 @@ def test_heading_parsing_no_date(self): h = Heading.parse_heading_from_data(text, self.allowed_todo_states) self.assertEqual(None, h.active_date) - text = ["* TODO This is a test <2011-08-25>"] - h = Heading.parse_heading_from_data(text, self.allowed_todo_states) - self.assertEqual(None, h.active_date) - - text = ["* TODO This is a test <2011-08-25 Wednesday>"] - h = Heading.parse_heading_from_data(text, self.allowed_todo_states) - self.assertEqual(None, h.active_date) - - text = ["* TODO This is a test <20110825>"] + text = ["* TODO This is a test"] h = Heading.parse_heading_from_data(text, self.allowed_todo_states) self.assertEqual(None, h.active_date) @@ -66,6 +62,7 @@ def test_heading_parsing_with_date(self): h = Heading.parse_heading_from_data(text, self.allowed_todo_states) self.assertEqual(odate, h.active_date) + def test_heading_parsing_with_date_and_body(self): """"" 'text' contains valid dates (in the body). @@ -148,6 +145,11 @@ def test_sorting_of_headings(self): [self.h1, self.h2_datetime, self.h2, self.h3, self.h_no_date], sorted([self.h2_datetime, self.h3, self.h2, self.h_no_date, self.h1])) + self.assertEqual( + [self.h1, self.h2_datetime, self.h2, self.h3, + self.h_no_date_2], + sorted([self.h2_datetime, self.h3, self.h2, + self.h_no_date_2, self.h1])) def suite(): return unittest.TestLoader().loadTestsFromTestCase( diff --git a/tests/test_liborgdate_parsing.py b/tests/test_liborgdate_parsing.py index ae49f0d0..3aa5f20e 100644 --- a/tests/test_liborgdate_parsing.py +++ b/tests/test_liborgdate_parsing.py @@ -90,15 +90,32 @@ def test_get_orgdate_parsing_inactive(self): result = get_orgdate(self.textinactive) self.assertNotEqual(result, None) self.assertTrue(isinstance(result, OrgDate)) - self.assertTrue(isinstance(get_orgdate(u"[2011-08-30 Tue]"), OrgDate)) - self.assertEqual(get_orgdate(u"[2011-08-30 Tue]").year, 2011) - self.assertEqual(get_orgdate(u"[2011-08-30 Tue]").month, 8) - self.assertEqual(get_orgdate(u"[2011-08-30 Tue]").day, 30) - self.assertFalse(get_orgdate(u"[2011-08-30 Tue]").active) + + text = u"[2011-08-30 Tue]" + expected_result = OrgDate(False, 2011, 8, 30) + result = get_orgdate(text) + self.assertTrue(isinstance(result, OrgDate)) + self.assertEqual(result, expected_result) + self.assertEqual(result.active == False, expected_result.active == False) datestr = u"This date [2011-08-30 Tue] is embedded" self.assertTrue(isinstance(get_orgdate(datestr), OrgDate)) + text = u"[2011-08-30]" + expected_result = OrgDate(False, 2011, 8, 30) + result = get_orgdate(text) + self.assertTrue(isinstance(result, OrgDate)) + self.assertEqual(result, expected_result) + self.assertEqual(result.active == False, expected_result.active == False) + + + text = u"[2011-08-30 Dienstag]" + expected_result = OrgDate(False, 2011, 8, 30) + result = get_orgdate(text) + self.assertTrue(isinstance(result, OrgDate)) + self.assertEqual(result, expected_result) + self.assertEqual(result.active == False, expected_result.active == False) + def test_get_orgdatetime_parsing_passive(self): u""" get_orgdate should recognize all orgdatetimes in a given text @@ -169,6 +186,7 @@ def test_get_orgdate_parsing_with_list_of_texts(self): self.assertEqual(result.minute, 10) def test_get_orgdate_parsing_with_invalid_input(self): + self.assertEquals(get_orgdate(u""), None) self.assertEquals(get_orgdate(u"NONSENSE"), None) self.assertEquals(get_orgdate(u"No D<2011- Date 08-29 Mon>"), None) self.assertEquals(get_orgdate(u"2011-08-r9 Mon]"), None) @@ -177,10 +195,11 @@ def test_get_orgdate_parsing_with_invalid_input(self): self.assertEquals(get_orgdate(u"2011-08-29 Mon"), None) self.assertEquals(get_orgdate(u"2011-08-29"), None) self.assertEquals(get_orgdate(u"2011-08-29 mon"), None) - self.assertEquals(get_orgdate(u"<2011-08-29 mon>"), None) + self.assertEquals(get_orgdate(u"<2011-08-r mon>"), None) + self.assertEquals(get_orgdate(u"<2011-08-29 m0n>"), None) - self.assertEquals(get_orgdate(u"wrong date embedded <2011-08-29 mon>"), None) - self.assertEquals(get_orgdate(u"wrong date <2011-08-29 mon>embedded "), None) + self.assertEquals(get_orgdate(u"wrong date embedded <2011-08-r9 mon>"), None) + self.assertEquals(get_orgdate(u"wrong date <2011-08-r9 mon>embedded "), None) def test_get_orgdate_parsing_with_invalid_dates(self): u""" diff --git a/tests/test_liborgdate_utf8.py b/tests/test_liborgdate_utf8.py index 079d7889..5251d2b8 100644 --- a/tests/test_liborgdate_utf8.py +++ b/tests/test_liborgdate_utf8.py @@ -18,7 +18,7 @@ class OrgDateUtf8TestCase(unittest.TestCase): Tests OrgDate with utf-8 enabled locales """ LOCALE_LOCK = threading.Lock() - UTF8_LOCALE = "pt_BR.utf-8" + UTF8_LOCALE = "es_ES.utf-8" @contextmanager def setlocale(self, name): @@ -33,8 +33,8 @@ def setUp(self): self.year = 2016 self.month = 5 self.day = 7 - self.text = u'<2016-05-07 Sáb>' - self.textinactive = u'[2016-05-07 Sáb]' + self.text = u'<2016-05-07 sáb>' + self.textinactive = u'[2016-05-07 sáb]' def test_OrdDate_str_unicode_active(self): with self.setlocale(self.UTF8_LOCALE):