From 49dcbc8746309ddc9f02bb9d72072d341fda87b3 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 20 Dec 2018 04:23:00 +0100 Subject: [PATCH 01/20] wip: implementing PLANNING Line support --- ftplugin/orgmode/liborgmode/dom_obj.py | 2 + ftplugin/orgmode/liborgmode/headings.py | 176 ++++++++++++++++++------ ftplugin/orgmode/plugins/Todo.py | 14 ++ 3 files changed, 152 insertions(+), 40 deletions(-) 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..cfdc90a9 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 = (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,82 @@ def active_date(self): """ return self._active_date + def render_planning_line(self, line=None): + matches = [] + if line: + matches = 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 = str(m) + if old: + line.replace(old, new_str) + else: + line = " ".join([new_str, str(line)]) + + + def update_planning_line(self): + self._is_dirty_body = True + 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/plugins/Todo.py b/ftplugin/orgmode/plugins/Todo.py index ad1a1a04..142296d7 100644 --- a/ftplugin/orgmode/plugins/Todo.py +++ b/ftplugin/orgmode/plugins/Todo.py @@ -144,6 +144,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 @@ -313,6 +322,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') From c893a95f7ebf5028d836e7d7ef40cb6e13c84eae Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 00:30:15 +0100 Subject: [PATCH 02/20] make has_planning_line a bool --- ftplugin/orgmode/liborgmode/headings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index cfdc90a9..469f182e 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -441,7 +441,7 @@ def parse_title(heading_line): 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 = (new_heading.scheduled_date or \ + new_heading.has_planning_line = bool (new_heading.scheduled_date or \ new_heading.deadline_date or \ new_heading.closed_date or \ data[1].strip() == "") From 8191e02a8cabe4a242a11dd0774a9b7ea2979e21 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 00:30:44 +0100 Subject: [PATCH 03/20] fix render planning line --- ftplugin/orgmode/liborgmode/headings.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index 469f182e..0c87f32b 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -648,7 +648,7 @@ def render_planning_line(self, line=None): for k in plannings: new_str = "" if plannings[k]: - new_str = "%s: %s" % (k, plannings[k]) + new_str = "%s: <%s>" % (k, plannings[k]) old = None for m in matches: @@ -657,11 +657,10 @@ def render_planning_line(self, line=None): if old: line.replace(old, new_str) else: - line = " ".join([new_str, str(line)]) - + line = line + " " + new_str + return line def update_planning_line(self): - self._is_dirty_body = True if self.has_planning_line: self.body[0] = self.render_planning_line(self.body[0]) else: From 6cad2abeaa51628cac5d607ebc5af4d27ed711b1 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 00:31:55 +0100 Subject: [PATCH 04/20] add plugin interface for Planning Line setting setting of SCHEDULED: and DEADLINE: Keywords --- ftplugin/org.vim | 34 +++++++++++++++++ ftplugin/orgmode/_vim.py | 48 ++++++++++++++++------- ftplugin/orgmode/plugins/Date.py | 65 ++++++++++++++++++++++++++++++-- 3 files changed, 130 insertions(+), 17 deletions(-) diff --git a/ftplugin/org.vim b/ftplugin/org.vim index f973ab63..af19e533 100644 --- a/ftplugin/org.vim +++ b/ftplugin/org.vim @@ -167,3 +167,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..8a0effa6 100644 --- a/ftplugin/orgmode/_vim.py +++ b/ftplugin/orgmode/_vim.py @@ -165,6 +165,37 @@ 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) + 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) + 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 +207,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 +230,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 +259,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/plugins/Date.py b/ftplugin/orgmode/plugins/Date.py index 04a675ee..25e52221 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. @@ -251,12 +252,29 @@ def insert_timestamp(cls, active=True): else: newdate = newdate.strftime( u_decode(u_encode(u'%Y-%m-%d %a'))) + return newdate + + + @classmethod + def insert_timestamp(cls, active=True): + newdate = cls.get_timestamp_cmdline() + 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 +288,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 +319,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'scs', + 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'scp', + 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'dls', + 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'dlp', + 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', From 307aa1b6a4fb6a64506be906fd9bd04a578e8b75 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 00:42:54 +0100 Subject: [PATCH 05/20] fix missing imports for calendar action --- ftplugin/org.vim | 1 + 1 file changed, 1 insertion(+) diff --git a/ftplugin/org.vim b/ftplugin/org.vim index af19e533..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 From 3828cd5dc2f6a01e33b98e9301234429b42bf3a6 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 01:48:48 +0100 Subject: [PATCH 06/20] fixing the planning-date value types heading.scheduled_date, heading.closed_date and heading.deadline_date should always be of OrgDate (or OrgDateTime) type. --- ftplugin/orgmode/_vim.py | 4 +++- ftplugin/orgmode/liborgmode/headings.py | 2 +- ftplugin/orgmode/plugins/Date.py | 13 +++++++------ 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ftplugin/orgmode/_vim.py b/ftplugin/orgmode/_vim.py index 8a0effa6..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 @@ -183,6 +183,7 @@ def set_scheduled_date(new_date): 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) @@ -193,6 +194,7 @@ def set_deadline_date(new_date): 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) diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index 0c87f32b..ae14c5bb 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -648,7 +648,7 @@ def render_planning_line(self, line=None): for k in plannings: new_str = "" if plannings[k]: - new_str = "%s: <%s>" % (k, plannings[k]) + new_str = "%s: %s" % (k, plannings[k]) old = None for m in matches: diff --git a/ftplugin/orgmode/plugins/Date.py b/ftplugin/orgmode/plugins/Date.py index 25e52221..4eb20aba 100644 --- a/ftplugin/orgmode/plugins/Date.py +++ b/ftplugin/orgmode/plugins/Date.py @@ -245,6 +245,13 @@ def get_timestamp_cmdline(cls): 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( @@ -252,12 +259,6 @@ def get_timestamp_cmdline(cls): else: newdate = newdate.strftime( u_decode(u_encode(u'%Y-%m-%d %a'))) - return newdate - - - @classmethod - def insert_timestamp(cls, active=True): - newdate = cls.get_timestamp_cmdline() timestamp = u'<%s>' % newdate if active else u'[%s]' % newdate From c00d74a0dafbbb4e363b96677d36d9b7dfbfafa6 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 02:25:53 +0100 Subject: [PATCH 07/20] fix rendering of planning line --- ftplugin/orgmode/liborgmode/headings.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index ae14c5bb..d25c04df 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -647,15 +647,15 @@ def render_planning_line(self, line=None): plannings = self.get_plannings() for k in plannings: new_str = "" - if plannings[k]: - new_str = "%s: %s" % (k, plannings[k]) - + if not plannings[k]: + continue + new_str = "%s: %s" % (k, plannings[k]) old = None for m in matches: if m.group(1) == k: - old = str(m) + old = m.group(0) if old: - line.replace(old, new_str) + line = line.replace(old, new_str) else: line = line + " " + new_str return line From b75d1ac84a77121ac8c472e9f68082b290ff20ed Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 02:26:06 +0100 Subject: [PATCH 08/20] relax parsing of TIMESTAMP data according to https://orgmode.org/worg/dev/org-syntax.html#Timestamp the dayname field can be pretty much anything. And indeed in german locale setting the dayname is abbreviated with 2 letters only --- ftplugin/orgmode/liborgmode/orgdate.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/ftplugin/orgmode/liborgmode/orgdate.py b/ftplugin/orgmode/liborgmode/orgdate.py index 93a6776a..19aa1726 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" % _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): From aab25ffefb0617aab0b05478b453ccb8ecf1f7b2 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 24 Dec 2018 02:36:40 +0100 Subject: [PATCH 09/20] document the mappings also make the mappings somewhat easier to remember by having the p in front mean calendar selection and having the s in front does mean cmdline selection. --- doc/orgguide.txt | 5 +++++ ftplugin/orgmode/plugins/Date.py | 8 ++++---- 2 files changed, 9 insertions(+), 4 deletions(-) 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/orgmode/plugins/Date.py b/ftplugin/orgmode/plugins/Date.py index 4eb20aba..0cf00f0d 100644 --- a/ftplugin/orgmode/plugins/Date.py +++ b/ftplugin/orgmode/plugins/Date.py @@ -323,28 +323,28 @@ def register(self): add_cmd_mapping_menu( self, name=u'OrgDateSetScheduledTimestampCmdLine', - key_mapping=u'scs', + 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'scp', + 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'dls', + 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'dlp', + 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' ) From d170765dd9192a17fd21e5443a0c88d4b2726705 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Tue, 25 Dec 2018 04:25:27 +0100 Subject: [PATCH 10/20] TODO Plugin: manage CLOSING: DateTime Manage the Plannings-Line CLOSING: Keyword When the TODO State changes from Done to not Done or vice versa --- ftplugin/orgmode/plugins/Todo.py | 15 ++++++++++++++- ftplugin/orgmode/vimbuffer.py | 6 ++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/ftplugin/orgmode/plugins/Todo.py b/ftplugin/orgmode/plugins/Todo.py index 142296d7..2e26d21b 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 = {} @@ -228,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 diff --git a/ftplugin/orgmode/vimbuffer.py b/ftplugin/orgmode/vimbuffer.py index b4760fb8..847a256b 100644 --- a/ftplugin/orgmode/vimbuffer.py +++ b/ftplugin/orgmode/vimbuffer.py @@ -89,6 +89,12 @@ def changedtick(self): def changedtick(self, value): self._changedtick = value + 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 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 From a61c52d8a96a468964424ce8f42212aedd195027 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Tue, 25 Dec 2018 04:50:51 +0100 Subject: [PATCH 11/20] fix bug in render_planning_line: finditer can only be iterated once so the fix is, that we save a list of matches instead of a reference to the iterator --- ftplugin/orgmode/liborgmode/headings.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index d25c04df..0131c0b1 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -642,8 +642,7 @@ def active_date(self): def render_planning_line(self, line=None): matches = [] if line: - matches = REGEX_PLANNING.finditer(line) - + matches = list(REGEX_PLANNING.finditer(line)) plannings = self.get_plannings() for k in plannings: new_str = "" From 26f04065fcef47fdfdfe1c5b4080d40fbcc53217 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Tue, 25 Dec 2018 04:52:24 +0100 Subject: [PATCH 12/20] fix render_planning to allow removal of keywords --- ftplugin/orgmode/liborgmode/headings.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index 0131c0b1..0e3917be 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -646,9 +646,8 @@ def render_planning_line(self, line=None): plannings = self.get_plannings() for k in plannings: new_str = "" - if not plannings[k]: - continue - new_str = "%s: %s" % (k, plannings[k]) + if plannings[k]: + new_str = "%s: %s" % (k, plannings[k]) old = None for m in matches: if m.group(1) == k: @@ -656,7 +655,9 @@ def render_planning_line(self, line=None): if old: line = line.replace(old, new_str) else: - line = line + " " + new_str + if new_str != "": + line = line + " " + new_str + return line def update_planning_line(self): From 011fbffc2a33b8cd3ceca2155bbe472990e6acd7 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Tue, 25 Dec 2018 17:59:02 +0100 Subject: [PATCH 13/20] fix render line The line=None case was broken. It turns out, that it works better, if line is empty string per default. --- ftplugin/orgmode/liborgmode/headings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ftplugin/orgmode/liborgmode/headings.py b/ftplugin/orgmode/liborgmode/headings.py index 0e3917be..ca4b36d3 100644 --- a/ftplugin/orgmode/liborgmode/headings.py +++ b/ftplugin/orgmode/liborgmode/headings.py @@ -639,7 +639,7 @@ def active_date(self): """ return self._active_date - def render_planning_line(self, line=None): + def render_planning_line(self, line=""): matches = [] if line: matches = list(REGEX_PLANNING.finditer(line)) @@ -655,7 +655,7 @@ def render_planning_line(self, line=None): if old: line = line.replace(old, new_str) else: - if new_str != "": + if new_str: line = line + " " + new_str return line From 9727d8c7d3516ebd44042e56b99da53e92bac44a Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 24 Jan 2019 12:56:29 +0100 Subject: [PATCH 14/20] adapt heading parse tests We do test heading parsing and not date-time parsing, so these cases should be enough. Sorting should ignore inactive dates -> test added. --- tests/test_libheading.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) 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( From e1f64eb6fcb21386fd470630df59c12bcd7d4fe3 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 24 Jan 2019 13:00:25 +0100 Subject: [PATCH 15/20] adapted testing according to: https://orgmode.org/worg/dev/org-syntax.org.html there are only a few restrictions on the DAYNAME part. (Which is wise for other locales). So <2011-08-29 mon> is a valid date. --- tests/test_liborgdate_parsing.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/test_liborgdate_parsing.py b/tests/test_liborgdate_parsing.py index ae49f0d0..d4d3615b 100644 --- a/tests/test_liborgdate_parsing.py +++ b/tests/test_liborgdate_parsing.py @@ -177,10 +177,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""" From 2a46acdadc1240254e766b977b8c0ef4087455c1 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 24 Jan 2019 13:58:39 +0100 Subject: [PATCH 16/20] Tests for OrgDate Parsing: Be liberal in what you tolerate on the receiving end --- tests/test_liborgdate_parsing.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/test_liborgdate_parsing.py b/tests/test_liborgdate_parsing.py index d4d3615b..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) From 00d9fb0d5fc08c84d3ab1dd4944001fd1062a6f3 Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 24 Jan 2019 14:07:23 +0100 Subject: [PATCH 17/20] liborgmode: more liberal OrgDate Parsing Allow missing day, because it is a reasonable human readable date form and we should probably brake no viable usecases with this. According to current formulation of the timestamp spec, a Date without Time is not valid anyway. --- ftplugin/orgmode/liborgmode/orgdate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ftplugin/orgmode/liborgmode/orgdate.py b/ftplugin/orgmode/liborgmode/orgdate.py index 19aa1726..d9151766 100644 --- a/ftplugin/orgmode/liborgmode/orgdate.py +++ b/ftplugin/orgmode/liborgmode/orgdate.py @@ -27,7 +27,7 @@ from orgmode.py3compat.encode_compatibility import * _DAYNAME = r"[^]>\s+0-9-]+" -_DATE = r"(\d\d\d\d)-(\d\d)-(\d\d) %s" % _DAYNAME +_DATE = r"(\d\d\d\d)-(\d\d)-(\d\d)(?:\s+%s)?" % _DAYNAME # <2011-09-12 Mon> _DATE_REGEX = re.compile(r"<%s>" %_DATE, re.UNICODE) From e05f4f39f0536224e30410f91ae03d6a6aa7ec3b Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 24 Jan 2019 14:28:55 +0100 Subject: [PATCH 18/20] fix utf8 test -> pt_br Locale day-names were changed to lowercase letters in this commit: https://sourceware.org/git/?p=glibc.git;a=commit;h=686db256f640372df81242bf20d458f54e069f56 --- tests/test_liborgdate_utf8.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_liborgdate_utf8.py b/tests/test_liborgdate_utf8.py index 079d7889..0f14d6ac 100644 --- a/tests/test_liborgdate_utf8.py +++ b/tests/test_liborgdate_utf8.py @@ -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): From bb1569d94937a9393f3e7a72f913486484cc920f Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Mon, 11 Feb 2019 22:53:28 +0100 Subject: [PATCH 19/20] fix utf-8 test by using another locale The pt_BR locale had the weekday capitalization changed [1]. That means that depending on libc version (-> Travis build tests vs Fedora 29) the test either was successull or failed [2]. [1]: https://sourceware.org/git/?p=glibc.git;a=commit;h=686db256f640372df81242bf20d458f54e069f56 [2]: commit e05f4f39f0536 re commit e05f4f39f0536 --- tests/test_liborgdate_utf8.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_liborgdate_utf8.py b/tests/test_liborgdate_utf8.py index 0f14d6ac..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): From 984df1a331e434d426b9fbfde00c95289ca9ae8c Mon Sep 17 00:00:00 2001 From: Florian Mickler Date: Thu, 23 May 2019 00:20:36 +0200 Subject: [PATCH 20/20] move todo state setting prsing a document function. --- ftplugin/orgmode/liborgmode/documents.py | 76 +++++++++++++++++++++++- ftplugin/orgmode/plugins/Todo.py | 2 +- ftplugin/orgmode/vimbuffer.py | 64 +------------------- 3 files changed, 75 insertions(+), 67 deletions(-) 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/plugins/Todo.py b/ftplugin/orgmode/plugins/Todo.py index 2e26d21b..34b8d8d6 100644 --- a/ftplugin/orgmode/plugins/Todo.py +++ b/ftplugin/orgmode/plugins/Todo.py @@ -292,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)] diff --git a/ftplugin/orgmode/vimbuffer.py b/ftplugin/orgmode/vimbuffer.py index 847a256b..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,68 +89,6 @@ def changedtick(self): def changedtick(self, value): self._changedtick = value - 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 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')))