/Plugins/UltiSnips/Plugin/pythonx/UltiSnips/text_objects/_base.py
Python | 386 lines | 379 code | 4 blank | 3 comment | 0 complexity | edf631d2dde1206785d218404ab5f825 MD5 | raw file
- #!/usr/bin/env python
- # encoding: utf-8
- """Base classes for all text objects."""
- from UltiSnips import _vim
- from UltiSnips.position import Position
- def _calc_end(text, start):
- """Calculate the end position of the 'text' starting at 'start."""
- if len(text) == 1:
- new_end = start + Position(0, len(text[0]))
- else:
- new_end = Position(start.line + len(text) - 1, len(text[-1]))
- return new_end
- def _text_to_vim(start, end, text):
- """Copy the given text to the current buffer, overwriting the span 'start'
- to 'end'."""
- lines = text.split('\n')
- new_end = _calc_end(lines, start)
- before = _vim.buf[start.line][:start.col]
- after = _vim.buf[end.line][end.col:]
- new_lines = []
- if len(lines):
- new_lines.append(before + lines[0])
- new_lines.extend(lines[1:])
- new_lines[-1] += after
- _vim.buf[start.line:end.line + 1] = new_lines
- # Open any folds this might have created
- _vim.buf.cursor = start
- _vim.command('normal! zv')
- return new_end
- # These classes use their subclasses a lot and we really do not want to expose
- # their functions more globally.
- # pylint: disable=protected-access
- class TextObject(object):
- """Represents any object in the text that has a span in any ways."""
- def __init__(self, parent, token_or_start, end=None,
- initial_text='', tiebreaker=None):
- self._parent = parent
- if end is not None: # Took 4 arguments
- self._start = token_or_start
- self._end = end
- self._initial_text = initial_text
- else: # Initialize from token
- self._start = token_or_start.start
- self._end = token_or_start.end
- self._initial_text = token_or_start.initial_text
- self._tiebreaker = tiebreaker or Position(
- self._start.line, self._end.line)
- if parent is not None:
- parent._add_child(self)
- def _move(self, pivot, diff):
- """Move this object by 'diff' while 'pivot' is the point of change."""
- self._start.move(pivot, diff)
- self._end.move(pivot, diff)
- def __lt__(self, other):
- me_tuple = (self.start.line, self.start.col,
- self._tiebreaker.line, self._tiebreaker.col)
- other_tuple = (other._start.line, other._start.col,
- other._tiebreaker.line, other._tiebreaker.col)
- return me_tuple < other_tuple
- def __le__(self, other):
- me_tuple = (self._start.line, self._start.col,
- self._tiebreaker.line, self._tiebreaker.col)
- other_tuple = (other._start.line, other._start.col,
- other._tiebreaker.line, other._tiebreaker.col)
- return me_tuple <= other_tuple
- def __repr__(self):
- ct = ''
- try:
- ct = self.current_text
- except IndexError:
- ct = '<err>'
- return '%s(%r->%r,%r)' % (self.__class__.__name__,
- self._start, self._end, ct)
- @property
- def current_text(self):
- """The current text of this object."""
- if self._start.line == self._end.line:
- return _vim.buf[self._start.line][self._start.col:self._end.col]
- else:
- lines = [_vim.buf[self._start.line][self._start.col:]]
- lines.extend(_vim.buf[self._start.line + 1:self._end.line])
- lines.append(_vim.buf[self._end.line][:self._end.col])
- return '\n'.join(lines)
- @property
- def start(self):
- """The start position."""
- return self._start
- @property
- def end(self):
- """The end position."""
- return self._end
- def overwrite(self, gtext=None):
- """Overwrite the text of this object in the Vim Buffer and update its
- length information.
- If 'gtext' is None use the initial text of this object.
- """
- # We explicitly do not want to move our children around here as we
- # either have non or we are replacing text initially which means we do
- # not want to mess with their positions
- if self.current_text == gtext:
- return
- old_end = self._end
- self._end = _text_to_vim(
- self._start, self._end, gtext or self._initial_text)
- if self._parent:
- self._parent._child_has_moved(
- self._parent._children.index(self), min(old_end, self._end),
- self._end.delta(old_end)
- )
- def _update(self, done):
- """Update this object inside the Vim Buffer.
- Return False if you need to be called again for this edit cycle.
- Otherwise return True.
- """
- raise NotImplementedError('Must be implemented by subclasses.')
- class EditableTextObject(TextObject):
- """This base class represents any object in the text that can be changed by
- the user."""
- def __init__(self, *args, **kwargs):
- TextObject.__init__(self, *args, **kwargs)
- self._children = []
- self._tabstops = {}
- ##############
- # Properties #
- ##############
- @property
- def children(self):
- """List of all children."""
- return self._children
- @property
- def _editable_children(self):
- """List of all children that are EditableTextObjects."""
- return [child for child in self._children if
- isinstance(child, EditableTextObject)]
- ####################
- # Public Functions #
- ####################
- def find_parent_for_new_to(self, pos):
- """Figure out the parent object for something at 'pos'."""
- for children in self._editable_children:
- if children._start <= pos < children._end:
- return children.find_parent_for_new_to(pos)
- if children._start == pos and pos == children._end:
- return children.find_parent_for_new_to(pos)
- return self
- ###############################
- # Private/Protected functions #
- ###############################
- def _do_edit(self, cmd, ctab=None):
- """Apply the edit 'cmd' to this object."""
- ctype, line, col, text = cmd
- assert ('\n' not in text) or (text == '\n')
- pos = Position(line, col)
- to_kill = set()
- new_cmds = []
- for child in self._children:
- if ctype == 'I': # Insertion
- if (child._start < pos <
- Position(child._end.line, child._end.col) and
- isinstance(child, NoneditableTextObject)):
- to_kill.add(child)
- new_cmds.append(cmd)
- break
- elif ((child._start <= pos <= child._end) and
- isinstance(child, EditableTextObject)):
- if pos == child.end and not child.children:
- try:
- if ctab.number != child.number:
- continue
- except AttributeError:
- pass
- child._do_edit(cmd, ctab)
- return
- else: # Deletion
- delend = pos + Position(0, len(text)) if text != '\n' \
- else Position(line + 1, 0)
- if ((child._start <= pos < child._end) and
- (child._start < delend <= child._end)):
- # this edit command is completely for the child
- if isinstance(child, NoneditableTextObject):
- to_kill.add(child)
- new_cmds.append(cmd)
- break
- else:
- child._do_edit(cmd, ctab)
- return
- elif ((pos < child._start and child._end <= delend and
- child.start < delend) or
- (pos <= child._start and child._end < delend)):
- # Case: this deletion removes the child
- to_kill.add(child)
- new_cmds.append(cmd)
- break
- elif (pos < child._start and
- (child._start < delend <= child._end)):
- # Case: partially for us, partially for the child
- my_text = text[:(child._start - pos).col]
- c_text = text[(child._start - pos).col:]
- new_cmds.append((ctype, line, col, my_text))
- new_cmds.append((ctype, line, col, c_text))
- break
- elif (delend >= child._end and (
- child._start <= pos < child._end)):
- # Case: partially for us, partially for the child
- c_text = text[(child._end - pos).col:]
- my_text = text[:(child._end - pos).col]
- new_cmds.append((ctype, line, col, c_text))
- new_cmds.append((ctype, line, col, my_text))
- break
- for child in to_kill:
- self._del_child(child)
- if len(new_cmds):
- for child in new_cmds:
- self._do_edit(child)
- return
- # We have to handle this ourselves
- delta = Position(1, 0) if text == '\n' else Position(0, len(text))
- if ctype == 'D':
- # Makes no sense to delete in empty textobject
- if self._start == self._end:
- return
- delta.line *= -1
- delta.col *= -1
- pivot = Position(line, col)
- idx = -1
- for cidx, child in enumerate(self._children):
- if child._start < pivot <= child._end:
- idx = cidx
- self._child_has_moved(idx, pivot, delta)
- def _move(self, pivot, diff):
- TextObject._move(self, pivot, diff)
- for child in self._children:
- child._move(pivot, diff)
- def _child_has_moved(self, idx, pivot, diff):
- """Called when a the child with 'idx' has moved behind 'pivot' by
- 'diff'."""
- self._end.move(pivot, diff)
- for child in self._children[idx + 1:]:
- child._move(pivot, diff)
- if self._parent:
- self._parent._child_has_moved(
- self._parent._children.index(self), pivot, diff
- )
- def _get_next_tab(self, number):
- """Returns the next tabstop after 'number'."""
- if not len(self._tabstops.keys()):
- return
- tno_max = max(self._tabstops.keys())
- possible_sol = []
- i = number + 1
- while i <= tno_max:
- if i in self._tabstops:
- possible_sol.append((i, self._tabstops[i]))
- break
- i += 1
- child = [c._get_next_tab(number) for c in self._editable_children]
- child = [c for c in child if c]
- possible_sol += child
- if not len(possible_sol):
- return None
- return min(possible_sol)
- def _get_prev_tab(self, number):
- """Returns the previous tabstop before 'number'."""
- if not len(self._tabstops.keys()):
- return
- tno_min = min(self._tabstops.keys())
- possible_sol = []
- i = number - 1
- while i >= tno_min and i > 0:
- if i in self._tabstops:
- possible_sol.append((i, self._tabstops[i]))
- break
- i -= 1
- child = [c._get_prev_tab(number) for c in self._editable_children]
- child = [c for c in child if c]
- possible_sol += child
- if not len(possible_sol):
- return None
- return max(possible_sol)
- def _get_tabstop(self, requester, number):
- """Returns the tabstop 'number'.
- 'requester' is the class that is interested in this.
- """
- if number in self._tabstops:
- return self._tabstops[number]
- for child in self._editable_children:
- if child is requester:
- continue
- rv = child._get_tabstop(self, number)
- if rv is not None:
- return rv
- if self._parent and requester is not self._parent:
- return self._parent._get_tabstop(self, number)
- def _update(self, done):
- if all((child in done) for child in self._children):
- assert self not in done
- done.add(self)
- return True
- def _add_child(self, child):
- """Add 'child' as a new child of this text object."""
- self._children.append(child)
- self._children.sort()
- def _del_child(self, child):
- """Delete this 'child'."""
- child._parent = None
- self._children.remove(child)
- # If this is a tabstop, delete it. Might have been deleted already if
- # it was nested.
- try:
- del self._tabstops[child.number]
- except (AttributeError, KeyError):
- pass
- class NoneditableTextObject(TextObject):
- """All passive text objects that the user can't edit by hand."""
- def _update(self, done):
- return True