503 lines
15 KiB
Python
503 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
vimbuffer
|
|
~~~~~~~~~~
|
|
|
|
VimBuffer and VimBufferContent are the interface between liborgmode and
|
|
vim.
|
|
|
|
VimBuffer extends the liborgmode.document.Document().
|
|
Document() is just a general implementation for loading an org file. It
|
|
has no interface to an actual file or vim buffer. This is the task of
|
|
vimbuffer.VimBuffer(). It is the interfaces to vim. The main tasks for
|
|
VimBuffer are to provide read and write access to a real vim buffer.
|
|
|
|
VimBufferContent is a helper class for VimBuffer. Basically, it hides the
|
|
details of encoding - everything read from or written to VimBufferContent
|
|
is UTF-8.
|
|
"""
|
|
|
|
try:
|
|
from collections import UserList
|
|
except:
|
|
from UserList import UserList
|
|
|
|
import vim
|
|
|
|
from orgmode import settings
|
|
from orgmode.exceptions import BufferNotFound, BufferNotInSync
|
|
from orgmode.liborgmode.documents import Document, MultiPurposeList, Direction
|
|
from orgmode.liborgmode.headings import Heading
|
|
|
|
from orgmode.py3compat.encode_compatibility import *
|
|
from orgmode.py3compat.unicode_compatibility import *
|
|
|
|
|
|
class VimBuffer(Document):
|
|
def __init__(self, bufnr=0):
|
|
u"""
|
|
:bufnr: 0: current buffer, every other number refers to another buffer
|
|
"""
|
|
Document.__init__(self)
|
|
self._bufnr = vim.current.buffer.number if bufnr == 0 else bufnr
|
|
self._changedtick = -1
|
|
self._cached_heading = None
|
|
if self._bufnr == vim.current.buffer.number:
|
|
self._content = VimBufferContent(vim.current.buffer)
|
|
else:
|
|
_buffer = None
|
|
for b in vim.buffers:
|
|
if self._bufnr == b.number:
|
|
_buffer = b
|
|
break
|
|
|
|
if not _buffer:
|
|
raise BufferNotFound(u'Unable to locate buffer number #%d' % self._bufnr)
|
|
self._content = VimBufferContent(_buffer)
|
|
|
|
self.update_changedtick()
|
|
self._orig_changedtick = self._changedtick
|
|
|
|
@property
|
|
def tabstop(self):
|
|
return int(vim.eval(u_encode(u'&ts')))
|
|
|
|
@property
|
|
def tag_column(self):
|
|
return int(settings.get(u'org_tag_column', u'77'))
|
|
|
|
@property
|
|
def is_insync(self):
|
|
if self._changedtick == self._orig_changedtick:
|
|
self.update_changedtick()
|
|
return self._changedtick == self._orig_changedtick
|
|
|
|
@property
|
|
def bufnr(self):
|
|
u"""
|
|
:returns: The buffer's number for the current document
|
|
"""
|
|
return self._bufnr
|
|
|
|
@property
|
|
def changedtick(self):
|
|
u""" Number of changes in vimbuffer """
|
|
return self._changedtick
|
|
|
|
@changedtick.setter
|
|
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')))
|
|
else:
|
|
vim.command(u_encode(u'unlet! g:org_changedtick | let g:org_lz = &lz | let g:org_hidden = &hidden | set lz hidden'))
|
|
# TODO is this likely to fail? maybe some error hangling should be added
|
|
vim.command(u_encode(u'keepalt buffer %d | let g:org_changedtick = b:changedtick | buffer %d' % \
|
|
(self.bufnr, vim.current.buffer.number)))
|
|
vim.command(u_encode(u'let &lz = g:org_lz | let &hidden = g:org_hidden | unlet! g:org_lz g:org_hidden | redraw'))
|
|
self._changedtick = int(vim.eval(u_encode(u'g:org_changedtick')))
|
|
|
|
def write(self):
|
|
u""" write the changes to the vim buffer
|
|
|
|
:returns: True if something was written, otherwise False
|
|
"""
|
|
if not self.is_dirty:
|
|
return False
|
|
|
|
self.update_changedtick()
|
|
if not self.is_insync:
|
|
raise BufferNotInSync(u'Buffer is not in sync with vim!')
|
|
|
|
# write meta information
|
|
if self.is_dirty_meta_information:
|
|
meta_end = 0 if self._orig_meta_information_len is None else self._orig_meta_information_len
|
|
self._content[:meta_end] = self.meta_information
|
|
self._orig_meta_information_len = len(self.meta_information)
|
|
|
|
# remove deleted headings
|
|
already_deleted = []
|
|
for h in sorted(self._deleted_headings, key=lambda x: x._orig_start, reverse=True):
|
|
if h._orig_start is not None and h._orig_start not in already_deleted:
|
|
# this is a heading that actually exists on the buffer and it
|
|
# needs to be removed
|
|
del self._content[h._orig_start:h._orig_start + h._orig_len]
|
|
already_deleted.append(h._orig_start)
|
|
del self._deleted_headings[:]
|
|
del already_deleted
|
|
|
|
# update changed headings and add new headings
|
|
for h in self.all_headings():
|
|
if h.is_dirty:
|
|
vim.current.buffer.append("") # workaround for neovim bug
|
|
if h._orig_start is not None:
|
|
# this is a heading that existed before and was changed. It
|
|
# needs to be replaced
|
|
if h.is_dirty_heading:
|
|
self._content[h.start:h.start + 1] = [unicode(h)]
|
|
if h.is_dirty_body:
|
|
self._content[h.start + 1:h.start + h._orig_len] = h.body
|
|
else:
|
|
# this is a new heading. It needs to be inserted
|
|
self._content[h.start:h.start] = [unicode(h)] + h.body
|
|
del vim.current.buffer[-1] # restore workaround for neovim bug
|
|
h._dirty_heading = False
|
|
h._dirty_body = False
|
|
# for all headings the length and start offset needs to be updated
|
|
h._orig_start = h.start
|
|
h._orig_len = len(h)
|
|
|
|
self._dirty_meta_information = False
|
|
self._dirty_document = False
|
|
|
|
self.update_changedtick()
|
|
self._orig_changedtick = self._changedtick
|
|
return True
|
|
|
|
def write_heading(self, heading, including_children=True):
|
|
""" WARNING: use this function only when you know what you are doing!
|
|
This function writes a heading to the vim buffer. It offers performance
|
|
advantages over the regular write() function. This advantage is
|
|
combined with no sanity checks! Whenever you use this function, make
|
|
sure the heading you are writing contains the right offsets
|
|
(Heading._orig_start, Heading._orig_len).
|
|
|
|
Usage example:
|
|
# Retrieve a potentially dirty document
|
|
d = ORGMODE.get_document(allow_dirty=True)
|
|
# Don't rely on the DOM, retrieve the heading afresh
|
|
h = d.find_heading(direction=Direction.FORWARD, position=100)
|
|
# Update tags
|
|
h.tags = ['tag1', 'tag2']
|
|
# Write the heading
|
|
d.write_heading(h)
|
|
|
|
This function can't be used to delete a heading!
|
|
|
|
:heading: Write this heading with to the vim buffer
|
|
:including_children: Also include children in the update
|
|
|
|
:returns The written heading
|
|
"""
|
|
if including_children and heading.children:
|
|
for child in heading.children[::-1]:
|
|
self.write_heading(child, including_children)
|
|
|
|
if heading.is_dirty:
|
|
if heading._orig_start is not None:
|
|
# this is a heading that existed before and was changed. It
|
|
# needs to be replaced
|
|
if heading.is_dirty_heading:
|
|
self._content[heading._orig_start:heading._orig_start + 1] = [unicode(heading)]
|
|
if heading.is_dirty_body:
|
|
self._content[heading._orig_start + 1:heading._orig_start + heading._orig_len] = heading.body
|
|
else:
|
|
# this is a new heading. It needs to be inserted
|
|
raise ValueError('Heading must contain the attribute _orig_start! %s' % heading)
|
|
heading._dirty_heading = False
|
|
heading._dirty_body = False
|
|
# for all headings the length offset needs to be updated
|
|
heading._orig_len = len(heading)
|
|
|
|
return heading
|
|
|
|
def write_checkbox(self, checkbox, including_children=True):
|
|
if including_children and checkbox.children:
|
|
for child in checkbox.children[::-1]:
|
|
self.write_checkbox(child, including_children)
|
|
|
|
if checkbox.is_dirty:
|
|
if checkbox._orig_start is not None:
|
|
# this is a heading that existed before and was changed. It
|
|
# needs to be replaced
|
|
# print "checkbox is dirty? " + str(checkbox.is_dirty_checkbox)
|
|
# print checkbox
|
|
if checkbox.is_dirty_checkbox:
|
|
self._content[checkbox._orig_start:checkbox._orig_start + 1] = [unicode(checkbox)]
|
|
if checkbox.is_dirty_body:
|
|
self._content[checkbox._orig_start + 1:checkbox._orig_start + checkbox._orig_len] = checkbox.body
|
|
else:
|
|
# this is a new checkbox. It needs to be inserted
|
|
raise ValueError('Checkbox must contain the attribute _orig_start! %s' % checkbox)
|
|
checkbox._dirty_checkbox = False
|
|
checkbox._dirty_body = False
|
|
# for all headings the length offset needs to be updated
|
|
checkbox._orig_len = len(checkbox)
|
|
|
|
return checkbox
|
|
|
|
def write_checkboxes(self, checkboxes):
|
|
pass
|
|
|
|
def previous_heading(self, position=None):
|
|
u""" Find the next heading (search forward) and return the related object
|
|
:returns: Heading object or None
|
|
"""
|
|
h = self.current_heading(position=position)
|
|
if h:
|
|
return h.previous_heading
|
|
|
|
def current_heading(self, position=None):
|
|
u""" Find the current heading (search backward) and return the related object
|
|
:returns: Heading object or None
|
|
"""
|
|
if position is None:
|
|
position = vim.current.window.cursor[0] - 1
|
|
|
|
if not self.headings:
|
|
return
|
|
|
|
def binaryFindInDocument():
|
|
hi = len(self.headings)
|
|
lo = 0
|
|
while lo < hi:
|
|
mid = (lo+hi)//2
|
|
h = self.headings[mid]
|
|
if h.end_of_last_child < position:
|
|
lo = mid + 1
|
|
elif h.start > position:
|
|
hi = mid
|
|
else:
|
|
return binaryFindHeading(h)
|
|
|
|
def binaryFindHeading(heading):
|
|
if not heading.children or heading.end >= position:
|
|
return heading
|
|
|
|
hi = len(heading.children)
|
|
lo = 0
|
|
while lo < hi:
|
|
mid = (lo+hi)//2
|
|
h = heading.children[mid]
|
|
if h.end_of_last_child < position:
|
|
lo = mid + 1
|
|
elif h.start > position:
|
|
hi = mid
|
|
else:
|
|
return binaryFindHeading(h)
|
|
|
|
# look at the cache to find the heading
|
|
h_tmp = self._cached_heading
|
|
if h_tmp is not None:
|
|
if h_tmp.end_of_last_child > position and \
|
|
h_tmp.start < position:
|
|
if h_tmp.end < position:
|
|
self._cached_heading = binaryFindHeading(h_tmp)
|
|
return self._cached_heading
|
|
|
|
self._cached_heading = binaryFindInDocument()
|
|
return self._cached_heading
|
|
|
|
def next_heading(self, position=None):
|
|
u""" Find the next heading (search forward) and return the related object
|
|
:returns: Heading object or None
|
|
"""
|
|
h = self.current_heading(position=position)
|
|
if h:
|
|
return h.next_heading
|
|
|
|
def find_current_heading(self, position=None, heading=Heading):
|
|
u""" Find the next heading backwards from the position of the cursor.
|
|
The difference to the function current_heading is that the returned
|
|
object is not built into the DOM. In case the DOM doesn't exist or is
|
|
out of sync this function is much faster in fetching the current
|
|
heading.
|
|
|
|
:position: The position to start the search from
|
|
|
|
:heading: The base class for the returned heading
|
|
|
|
:returns: Heading object or None
|
|
"""
|
|
return self.find_heading(vim.current.window.cursor[0] - 1 \
|
|
if position is None else position, \
|
|
direction=Direction.BACKWARD, heading=heading, \
|
|
connect_with_document=False)
|
|
|
|
|
|
class VimBufferContent(MultiPurposeList):
|
|
u""" Vim Buffer Content is a UTF-8 wrapper around a vim buffer. When
|
|
retrieving or setting items in the buffer an automatic conversion is
|
|
performed.
|
|
|
|
This ensures UTF-8 usage on the side of liborgmode and the vim plugin
|
|
vim-orgmode.
|
|
"""
|
|
|
|
def __init__(self, vimbuffer, on_change=None):
|
|
MultiPurposeList.__init__(self, on_change=on_change)
|
|
|
|
# replace data with vimbuffer to make operations change the actual
|
|
# buffer
|
|
self.data = vimbuffer
|
|
|
|
def __contains__(self, item):
|
|
i = item
|
|
if type(i) is unicode:
|
|
i = u_encode(item)
|
|
return MultiPurposeList.__contains__(self, i)
|
|
|
|
def __getitem__(self, i):
|
|
if isinstance(i, slice):
|
|
return [u_decode(item) if type(item) is str else item \
|
|
for item in MultiPurposeList.__getitem__(self, i)]
|
|
else:
|
|
item = MultiPurposeList.__getitem__(self, i)
|
|
if type(item) is str:
|
|
return u_decode(item)
|
|
return item
|
|
|
|
def __setitem__(self, i, item):
|
|
if isinstance(i, slice):
|
|
o = []
|
|
o_tmp = item
|
|
if type(o_tmp) not in (list, tuple) and not isinstance(o_tmp, UserList):
|
|
o_tmp = list(o_tmp)
|
|
for item in o_tmp:
|
|
if type(item) == unicode:
|
|
o.append(u_encode(item))
|
|
else:
|
|
o.append(item)
|
|
MultiPurposeList.__setitem__(self, i, o)
|
|
else:
|
|
_i = item
|
|
if type(_i) is unicode:
|
|
_i = u_encode(item)
|
|
|
|
# TODO: fix this bug properly, it is really strange that it fails on
|
|
# python3 without it. Problem is that when _i = ['* '] it fails in
|
|
# UserList.__setitem__() but if it is changed in debuggr in __setitem__
|
|
# like item[0] = '* ' it works, hence this is some quirk with unicode
|
|
# stuff but very likely vim 7.4 BUG too.
|
|
if isinstance(_i, UserList) and sys.version_info > (3, ):
|
|
_i = [s.encode('utf8').decode('utf8') for s in _i]
|
|
|
|
MultiPurposeList.__setitem__(self, i, _i)
|
|
|
|
def __add__(self, other):
|
|
raise NotImplementedError()
|
|
# TODO: implement me
|
|
if isinstance(other, UserList):
|
|
return self.__class__(self.data + other.data)
|
|
elif isinstance(other, type(self.data)):
|
|
return self.__class__(self.data + other)
|
|
else:
|
|
return self.__class__(self.data + list(other))
|
|
|
|
def __radd__(self, other):
|
|
raise NotImplementedError()
|
|
# TODO: implement me
|
|
if isinstance(other, UserList):
|
|
return self.__class__(other.data + self.data)
|
|
elif isinstance(other, type(self.data)):
|
|
return self.__class__(other + self.data)
|
|
else:
|
|
return self.__class__(list(other) + self.data)
|
|
|
|
def __iadd__(self, other):
|
|
o = []
|
|
o_tmp = other
|
|
if type(o_tmp) not in (list, tuple) and not isinstance(o_tmp, UserList):
|
|
o_tmp = list(o_tmp)
|
|
for i in o_tmp:
|
|
if type(i) is unicode:
|
|
o.append(u_encode(i))
|
|
else:
|
|
o.append(i)
|
|
|
|
return MultiPurposeList.__iadd__(self, o)
|
|
|
|
def append(self, item):
|
|
i = item
|
|
if type(item) is str:
|
|
i = u_encode(item)
|
|
MultiPurposeList.append(self, i)
|
|
|
|
def insert(self, i, item):
|
|
_i = item
|
|
if type(_i) is str:
|
|
_i = u_encode(item)
|
|
MultiPurposeList.insert(self, i, _i)
|
|
|
|
def index(self, item, *args):
|
|
i = item
|
|
if type(i) is unicode:
|
|
i = u_encode(item)
|
|
MultiPurposeList.index(self, i, *args)
|
|
|
|
def pop(self, i=-1):
|
|
return u_decode(MultiPurposeList.pop(self, i))
|
|
|
|
def extend(self, other):
|
|
o = []
|
|
o_tmp = other
|
|
if type(o_tmp) not in (list, tuple) and not isinstance(o_tmp, UserList):
|
|
o_tmp = list(o_tmp)
|
|
for i in o_tmp:
|
|
if type(i) is unicode:
|
|
o.append(u_encode(i))
|
|
else:
|
|
o.append(i)
|
|
MultiPurposeList.extend(self, o)
|
|
|
|
|
|
# vim: set noexpandtab:
|