#@+leo-ver=5-thin
#@+node:ekr.20031218072017.3603: * @file leoUndo.py
'''Leo's undo/redo manager.'''
#@+<< How Leo implements unlimited undo >>
#@+node:ekr.20031218072017.2413: ** << How Leo implements unlimited undo >>
#@+at Think of the actions that may be Undone or Redone as a string of beads
# (g.Bunches) containing all information needed to undo _and_ redo an operation.
#
# A bead pointer points to the present bead. Undoing an operation moves the bead
# pointer backwards; redoing an operation moves the bead pointer forwards. The
# bead pointer points in front of the first bead when Undo is disabled. The bead
# pointer points at the last bead when Redo is disabled.
#
# The Undo command uses the present bead to undo the action, then moves the bead
# pointer backwards. The Redo command uses the bead after the present bead to redo
# the action, then moves the bead pointer forwards. The list of beads does not
# branch; all undoable operations (except the Undo and Redo commands themselves)
# delete any beads following the newly created bead.
#
# New in Leo 4.3: User (client) code should call u.beforeX and u.afterX methods to
# create a bead describing the operation that is being performed. (By convention,
# the code sets u = c.undoer for undoable operations.) Most u.beforeX methods
# return 'undoData' that the client code merely passes to the corresponding
# u.afterX method. This data contains the 'before' snapshot. The u.afterX methods
# then create a bead containing both the 'before' and 'after' snapshots.
#
# New in Leo 4.3: u.beforeChangeGroup and u.afterChangeGroup allow multiple calls
# to u.beforeX and u.afterX methods to be treated as a single undoable entry. See
# the code for the Replace All, Sort, Promote and Demote commands for examples.
# u.before/afterChangeGroup substantially reduce the number of u.before/afterX
# methods needed.
#
# New in Leo 4.3: It would be possible for plugins or other code to define their
# own u.before/afterX methods. Indeed, u.afterX merely needs to set the
# bunch.undoHelper and bunch.redoHelper ivars to the methods used to undo and redo
# the operation. See the code for the various u.before/afterX methods for
# guidance.
#
# New in Leo 4.3: p.setDirty and p.setAllAncestorAtFileNodesDirty now return a
# 'dirtyVnodeList' that all vnodes that became dirty as the result of an
# operation. More than one list may be generated: client code is responsible for
# merging lists using the pattern dirtyVnodeList.extend(dirtyVnodeList2)
#
# I first saw this model of unlimited undo in the documentation for Apple's Yellow Box classes.
#@-<< How Leo implements unlimited undo >>
import leo.core.leoGlobals as g
# pylint: disable=unpacking-non-sequence
#@+others
#@+node:ekr.20031218072017.3605: ** class Undoer
[docs]class Undoer(object):
"""A class that implements unlimited undo and redo."""
# pylint: disable=not-an-iterable
# pylint: disable=unsubscriptable-object
# So that ivars can be inited to None rather thatn [].
#@+others
#@+node:ekr.20150509193307.1: *3* u.Birth
#@+node:ekr.20031218072017.3606: *4* u.__init__ & reloadSettings
def __init__(self, c):
self.c = c
self.debug_Undoer = False # True: enable debugging code in new undo scheme.
self.debug_print = False # True: enable print statements in debug code.
self.granularity = None # Set in reloadSettings.
self.max_undo_stack_size = c.config.getInt('max_undo_stack_size') or 0
# Statistics comparing old and new ways (only if self.debug_Undoer is on).
self.new_mem = 0
self.old_mem = 0
# State ivars...
self.beads = [] # List of undo nodes.
self.bead = -1 # Index of the present bead: -1:len(beads)
self.undoType = "Can't Undo"
# These must be set here, _not_ in clearUndoState.
self.redoMenuLabel = "Can't Redo"
self.undoMenuLabel = "Can't Undo"
self.realRedoMenuLabel = "Can't Redo"
self.realUndoMenuLabel = "Can't Undo"
self.undoing = False # True if executing an Undo command.
self.redoing = False # True if executing a Redo command.
self.per_node_undo = False # True: v may contain undo_info ivar.
# New in 4.2...
self.optionalIvars = []
# Set the following ivars to keep pylint happy.
self.afterTree = None
self.beforeTree = None
self.children = None
self.deleteMarkedNodesData = None
self.dirtyVnodeList = None
self.followingSibs = None
self.inHead = None
self.kind = None
self.newBack = None
self.newBody = None
self.newChanged = None
self.newChildren = None
self.newHead = None
self.newMarked = None
self.newN = None
self.newP = None
self.newParent = None
self.newParent_v = None
self.newRecentFiles = None
self.newSel = None
self.newTree = None
self.newYScroll = None
self.oldBack = None
self.oldBody = None
self.oldChanged = None
self.oldChildren = None
self.oldHead = None
self.oldMarked = None
self.oldN = None
self.oldParent = None
self.oldParent_v = None
self.oldRecentFiles = None
self.oldSel = None
self.oldTree = None
self.oldYScroll = None
self.pasteAsClone = None
self.prevSel = None
self.sortChildren = None
self.verboseUndoGroup = None
self.reloadSettings()
[docs] def reloadSettings(self):
'''Undoer.reloadSettings.'''
c = self.c
self.granularity = c.config.getString('undo_granularity')
if self.granularity:
self.granularity = self.granularity.lower()
if self.granularity not in ('node', 'line', 'word', 'char'):
self.granularity = 'line'
[docs] def redoHelper(self):
pass
[docs] def undoHelper(self):
pass
#@+node:ekr.20150509193222.1: *4* u.cmd (decorator)
[docs] def cmd(name):
'''Command decorator for the Undoer class.'''
# pylint: disable=no-self-argument
return g.new_cmd_decorator(name, ['c', 'undoer', ])
#@+node:ekr.20050416092908.1: *3* u.Internal helpers
#@+node:ekr.20031218072017.3607: *4* u.clearOptionalIvars
[docs] def clearOptionalIvars(self):
u = self
u.p = None # The position/node being operated upon for undo and redo.
for ivar in u.optionalIvars:
setattr(u, ivar, None)
#@+node:ekr.20060127052111.1: *4* u.cutStack
[docs] def cutStack(self):
u = self; n = u.max_undo_stack_size
if n > 0 and u.bead >= n and not g.app.unitTesting:
# Do nothing if we are in the middle of creating a group.
i = len(u.beads) - 1
while i >= 0:
bunch = u.beads[i]
if hasattr(bunch, 'kind') and bunch.kind == 'beforeGroup':
return
i -= 1
# This work regardless of how many items appear after bead n.
# g.trace('Cutting undo stack to %d entries' % (n))
u.beads = u.beads[-n:]
u.bead = n - 1
#@+node:ekr.20080623083646.10: *4* u.dumpBead
[docs] def dumpBead(self, n):
u = self
if n < 0 or n >= len(u.beads):
return 'no bead: n = ', n
# bunch = u.beads[n]
result = []
result.append('-' * 10)
result.append('len(u.beads): %s, n: %s' % (len(u.beads), n))
for ivar in ('kind', 'newP', 'newN', 'p', 'oldN', 'undoHelper'):
result.append('%s = %s' % (ivar, getattr(self, ivar)))
return '\n'.join(result)
[docs] def dumpTopBead(self):
u = self
n = len(u.beads)
if n > 0:
return self.dumpBead(n - 1)
else:
return '<no top bead>'
#@+node:EKR.20040526150818: *4* u.getBead
[docs] def getBead(self, n):
'''Set Undoer ivars from the bunch at the top of the undo stack.'''
u = self
if n < 0 or n >= len(u.beads):
return None
bunch = u.beads[n]
self.setIvarsFromBunch(bunch)
return bunch
#@+node:EKR.20040526150818.1: *4* u.peekBead
[docs] def peekBead(self, n):
u = self
if n < 0 or n >= len(u.beads):
return None
else:
return u.beads[n]
#@+node:ekr.20060127113243: *4* u.pushBead
[docs] def pushBead(self, bunch):
u = self
# New in 4.4b2: Add this to the group if it is being accumulated.
bunch2 = u.bead >= 0 and u.bead < len(u.beads) and u.beads[u.bead]
if bunch2 and hasattr(bunch2, 'kind') and bunch2.kind == 'beforeGroup':
# Just append the new bunch the group's items.
bunch2.items.append(bunch)
else:
# Push the bunch.
u.bead += 1
u.beads[u.bead:] = [bunch]
# Recalculate the menu labels.
u.setUndoTypes()
#@+node:ekr.20050126081529: *4* u.recognizeStartOfTypingWord
[docs] def recognizeStartOfTypingWord(self,
old_lines, old_row, old_col, old_ch,
new_lines, new_row, new_col, new_ch,
prev_row, prev_col
):
''' A potentially user-modifiable method that should return True if the
typing indicated by the params starts a new 'word' for the purposes of
undo with 'word' granularity.
u.setUndoTypingParams calls this method only when the typing could possibly
continue a previous word. In other words, undo will work safely regardless
of the value returned here.
old_ch is the char at the given (Tk) row, col of old_lines.
new_ch is the char at the given (Tk) row, col of new_lines.
The present code uses only old_ch and new_ch. The other arguments are given
for use by more sophisticated algorithms.'''
# Start a word if new_ch begins whitespace + word
new_word_started = not old_ch.isspace() and new_ch.isspace()
# Start a word if the cursor has been moved since the last change
moved_cursor = new_row != prev_row or new_col != prev_col + 1
return new_word_started or moved_cursor
#@+node:ekr.20031218072017.3613: *4* u.redoMenuName, undoMenuName
#@+node:ekr.20060127070008: *4* u.setIvarsFromBunch
[docs] def setIvarsFromBunch(self, bunch):
u = self
u.clearOptionalIvars()
if 0: # Debugging.
g.pr('-' * 40)
for key in sorted(bunch):
g.trace(key, bunch.get(key))
g.pr('-' * 20)
# bunch is not a dict, so bunch.keys() is required.
for key in list(bunch.keys()):
val = bunch.get(key)
setattr(u, key, val)
if key not in u.optionalIvars:
u.optionalIvars.append(key)
#@+node:ekr.20031218072017.3614: *4* u.setRedoType
# These routines update both the ivar and the menu label.
[docs] def setRedoType(self, theType):
u = self; frame = u.c.frame
if not g.isString(theType):
g.trace('oops: expected string for command, got %s' % repr(theType))
g.trace(g.callers())
theType = '<unknown>'
menu = frame.menu.getMenu("Edit")
name = u.redoMenuName(theType)
if name != u.redoMenuLabel:
# Update menu using old name.
realLabel = frame.menu.getRealMenuName(name)
if realLabel == name:
underline = -1 if g.match(name, 0, "Can't") else 0
else:
underline = realLabel.find("&")
realLabel = realLabel.replace("&", "")
frame.menu.setMenuLabel(menu, u.realRedoMenuLabel, realLabel, underline=underline)
u.redoMenuLabel = name
u.realRedoMenuLabel = realLabel
#@+node:ekr.20091221145433.6381: *4* u.setUndoType
[docs] def setUndoType(self, theType):
u = self; frame = u.c.frame
if not g.isString(theType):
g.trace('oops: expected string for command, got %s' % repr(theType))
g.trace(g.callers())
theType = '<unknown>'
menu = frame.menu.getMenu("Edit")
name = u.undoMenuName(theType)
if name != u.undoMenuLabel:
# Update menu using old name.
realLabel = frame.menu.getRealMenuName(name)
if realLabel == name:
underline = -1 if g.match(name, 0, "Can't") else 0
else:
underline = realLabel.find("&")
realLabel = realLabel.replace("&", "")
frame.menu.setMenuLabel(menu, u.realUndoMenuLabel, realLabel, underline=underline)
u.undoType = theType
u.undoMenuLabel = name
u.realUndoMenuLabel = realLabel
#@+node:ekr.20031218072017.3616: *4* u.setUndoTypes
[docs] def setUndoTypes(self):
u = self
# Set the undo type and undo menu label.
bunch = u.peekBead(u.bead)
if bunch:
u.setUndoType(bunch.undoType)
else:
u.setUndoType("Can't Undo")
# Set only the redo menu label.
bunch = u.peekBead(u.bead + 1)
if bunch:
u.setRedoType(bunch.undoType)
else:
u.setRedoType("Can't Redo")
u.cutStack()
#@+node:EKR.20040530121329: *4* u.restoreTree & helpers
[docs] def restoreTree(self, treeInfo):
"""Use the tree info to restore all VNode data,
including all links."""
u = self
# This effectively relinks all vnodes.
for v, vInfo, tInfo in treeInfo:
u.restoreVnodeUndoInfo(vInfo)
u.restoreTnodeUndoInfo(tInfo)
#@+node:ekr.20050415170737.2: *5* u.restoreVnodeUndoInfo
[docs] def restoreVnodeUndoInfo(self, bunch):
"""Restore all ivars saved in the bunch."""
v = bunch.v
v.statusBits = bunch.statusBits
v.children = bunch.children
v.parents = bunch.parents
uA = bunch.get('unknownAttributes')
if uA is not None:
v.unknownAttributes = uA
v._p_changed = 1
#@+node:ekr.20050415170812.2: *5* u.restoreTnodeUndoInfo
[docs] def restoreTnodeUndoInfo(self, bunch):
v = bunch.v
v.h = bunch.headString
v.b = bunch.bodyString
v.statusBits = bunch.statusBits
uA = bunch.get('unknownAttributes')
if uA is not None:
v.unknownAttributes = uA
v._p_changed = 1
#@+node:EKR.20040528075307: *4* u.saveTree & helpers
[docs] def saveTree(self, p, treeInfo=None):
"""Return a list of tuples with all info needed to handle a general undo operation."""
# WARNING: read this before doing anything "clever"
#@+<< about u.saveTree >>
#@+node:EKR.20040530114124: *5* << about u.saveTree >>
#@+at The old code made a free-standing copy of the tree using v.copy and
# t.copy. This looks "elegant" and is WRONG. The problem is that it can
# not handle clones properly, especially when some clones were in the
# "undo" tree and some were not. Moreover, it required complex
# adjustments to t.vnodeLists.
#
# Instead of creating new nodes, the new code creates all information
# needed to properly restore the vnodes and tnodes. It creates a list of
# tuples, on tuple for each VNode in the tree. Each tuple has the form,
#
# (vnodeInfo, tnodeInfo)
#
# where vnodeInfo and tnodeInfo are dicts contain all info needed to
# recreate the nodes. The v.createUndoInfoDict and t.createUndoInfoDict
# methods correspond to the old v.copy and t.copy methods.
#
# Aside: Prior to 4.2 Leo used a scheme that was equivalent to the
# createUndoInfoDict info, but quite a bit uglier.
#@-<< about u.saveTree >>
u = self; topLevel = (treeInfo is None)
if topLevel: treeInfo = []
# Add info for p.v. Duplicate tnode info is harmless.
data = (p.v, u.createVnodeUndoInfo(p.v), u.createTnodeUndoInfo(p.v))
treeInfo.append(data)
# Recursively add info for the subtree.
child = p.firstChild()
while child:
self.saveTree(child, treeInfo)
child = child.next()
return treeInfo
#@+node:ekr.20050415170737.1: *5* u.createVnodeUndoInfo
[docs] def createVnodeUndoInfo(self, v):
"""Create a bunch containing all info needed to recreate a VNode for undo."""
bunch = g.Bunch(
v=v,
statusBits=v.statusBits,
parents=v.parents[:],
children=v.children[:],
)
if hasattr(v, 'unknownAttributes'):
bunch.unknownAttributes = v.unknownAttributes
return bunch
#@+node:ekr.20050415170812.1: *5* u.createTnodeUndoInfo
[docs] def createTnodeUndoInfo(self, v):
"""Create a bunch containing all info needed to recreate a VNode."""
bunch = g.Bunch(
v=v,
headString=v.h,
bodyString=v.b,
statusBits=v.statusBits,
)
if hasattr(v, 'unknownAttributes'):
bunch.unknownAttributes = v.unknownAttributes
return bunch
#@+node:ekr.20050525151449: *4* u.trace
[docs] def trace(self):
ivars = ('kind', 'undoType')
for ivar in ivars:
g.pr(ivar, getattr(self, ivar))
#@+node:ekr.20050410095424: *4* u.updateMarks
[docs] def updateMarks(self, oldOrNew):
'''Update dirty and marked bits.'''
u = self; c = u.c
if oldOrNew not in ('new', 'old'):
g.trace("can't happen")
return
isOld = oldOrNew == 'old'
marked = u.oldMarked if isOld else u.newMarked
if marked: c.setMarked(u.p)
else: c.clearMarked(u.p)
# Bug fix: Leo 4.4.6: Undo/redo always set changed/dirty bits
# because the file may have been saved.
u.p.setDirty(setDescendentsDirty=False)
u.p.setAllAncestorAtFileNodesDirty(setDescendentsDirty=False) # Bug fix: Leo 4.4.6
u.c.setChanged(True)
#@+node:ekr.20031218072017.3608: *3* u.Externally visible entries
#@+node:ekr.20050318085432.4: *4* u.afterX...
#@+node:ekr.20050315134017.4: *5* u.afterChangeGroup
[docs] def afterChangeGroup(self, p, undoType, reportFlag=False, dirtyVnodeList=None):
'''Create an undo node for general tree operations using d created by beforeChangeGroup'''
u = self; c = self.c
w = c.frame.body.wrapper
if u.redoing or u.undoing:
return
if dirtyVnodeList is None: dirtyVnodeList = []
bunch = u.beads[u.bead]
if not u.beads:
g.trace('oops: empty undo stack.')
return
if bunch.kind == 'beforeGroup':
bunch.kind = 'afterGroup'
else:
g.trace('oops: expecting beforeGroup, got %s' % bunch.kind)
# Set the types & helpers.
bunch.kind = 'afterGroup'
bunch.undoType = undoType
# Set helper only for undo:
# The bead pointer will point to an 'beforeGroup' bead for redo.
bunch.undoHelper = u.undoGroup
bunch.redoHelper = u.redoGroup
bunch.dirtyVnodeList = dirtyVnodeList
bunch.newP = p.copy()
bunch.newSel = w.getSelectionRange()
# Tells whether to report the number of separate changes undone/redone.
bunch.reportFlag = reportFlag
if 0:
# Push the bunch.
u.bead += 1
u.beads[u.bead:] = [bunch]
# Recalculate the menu labels.
u.setUndoTypes()
#@+node:ekr.20050315134017.2: *5* u.afterChangeNodeContents
[docs] def afterChangeNodeContents(self, p, command, bunch, dirtyVnodeList=None, inHead=False):
'''Create an undo node using d created by beforeChangeNode.'''
u = self; c = self.c; w = c.frame.body.wrapper
if u.redoing or u.undoing:
return
if dirtyVnodeList is None: dirtyVnodeList = []
# Set the type & helpers.
bunch.kind = 'node'
bunch.undoType = command
bunch.undoHelper = u.undoNodeContents
bunch.redoHelper = u.redoNodeContents
bunch.dirtyVnodeList = dirtyVnodeList
bunch.inHead = inHead # 2013/08/26
bunch.newBody = p.b
bunch.newChanged = u.c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newHead = p.h
bunch.newMarked = p.isMarked()
# Bug fix 2017/11/12: don't use ternary operator.
if w:
bunch.newSel = w.getSelectionRange()
else:
bunch.newSel = 0, 0
bunch.newYScroll = w.getYScrollPosition() if w else 0
u.pushBead(bunch)
#@+node:ekr.20050315134017.3: *5* u.afterChangeTree
[docs] def afterChangeTree(self, p, command, bunch):
'''Create an undo node for general tree operations using d created by beforeChangeTree'''
u = self; c = self.c; w = c.frame.body.wrapper
if u.redoing or u.undoing: return
# Set the types & helpers.
bunch.kind = 'tree'
bunch.undoType = command
bunch.undoHelper = u.undoTree
bunch.redoHelper = u.redoTree
# Set by beforeChangeTree: changed, oldSel, oldText, oldTree, p
bunch.newSel = w.getSelectionRange()
bunch.newText = w.getAllText()
bunch.newTree = u.saveTree(p)
u.pushBead(bunch)
#@+node:ekr.20050424161505: *5* u.afterClearRecentFiles
[docs] def afterClearRecentFiles(self, bunch):
u = self
bunch.newRecentFiles = g.app.config.recentFiles[:]
bunch.undoType = 'Clear Recent Files'
bunch.undoHelper = u.undoClearRecentFiles
bunch.redoHelper = u.redoClearRecentFiles
u.pushBead(bunch)
return bunch
#@+node:ekr.20111006060936.15639: *5* u.afterCloneMarkedNodes
[docs] def afterCloneMarkedNodes(self, p):
u = self; c = u.c
if u.redoing or u.undoing:
return
bunch = u.createCommonBunch(p)
# Sets
# oldChanged = c.isChanged(),
# oldDirty = p.isDirty(),
# oldMarked = p.isMarked(),
# oldSel = w and w.getSelectionRange() or None,
# p = p.copy(),
# Set types & helpers
bunch.kind = 'clone-marked-nodes'
bunch.undoType = 'clone-marked-nodes'
# Set helpers
bunch.undoHelper = u.undoCloneMarkedNodes
bunch.redoHelper = u.redoCloneMarkedNodes
bunch.newP = p.next()
bunch.newChanged = c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
u.pushBead(bunch)
#@+node:ekr.20160502175451.1: *5* u.afterCopyMarkedNodes
[docs] def afterCopyMarkedNodes(self, p):
u = self; c = u.c
if u.redoing or u.undoing:
return
bunch = u.createCommonBunch(p)
# Sets
# oldChanged = c.isChanged(),
# oldDirty = p.isDirty(),
# oldMarked = p.isMarked(),
# oldSel = w and w.getSelectionRange() or None,
# p = p.copy(),
# Set types & helpers
bunch.kind = 'copy-marked-nodes'
bunch.undoType = 'copy-marked-nodes'
# Set helpers
bunch.undoHelper = u.undoCopyMarkedNodes
bunch.redoHelper = u.redoCopyMarkedNodes
bunch.newP = p.next()
bunch.newChanged = c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
u.pushBead(bunch)
#@+node:ekr.20050411193627.5: *5* u.afterCloneNode
[docs] def afterCloneNode(self, p, command, bunch, dirtyVnodeList=None):
u = self; c = u.c
if u.redoing or u.undoing: return
if dirtyVnodeList is None: dirtyVnodeList = []
# Set types & helpers
bunch.kind = 'clone'
bunch.undoType = command
# Set helpers
bunch.undoHelper = u.undoCloneNode
bunch.redoHelper = u.redoCloneNode
bunch.newBack = p.back() # 6/15/05
bunch.newParent = p.parent() # 6/15/05
bunch.newP = p.copy()
bunch.dirtyVnodeList = dirtyVnodeList
bunch.newChanged = c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
u.pushBead(bunch)
#@+node:ekr.20050411193627.6: *5* u.afterDehoist
[docs] def afterDehoist(self, p, command):
u = self
if u.redoing or u.undoing: return
bunch = u.createCommonBunch(p)
# Set types & helpers
bunch.kind = 'dehoist'
bunch.undoType = command
# Set helpers
bunch.undoHelper = u.undoDehoistNode
bunch.redoHelper = u.redoDehoistNode
u.pushBead(bunch)
#@+node:ekr.20050411193627.8: *5* u.afterDeleteNode
[docs] def afterDeleteNode(self, p, command, bunch, dirtyVnodeList=None):
u = self; c = u.c
if u.redoing or u.undoing:
return
if dirtyVnodeList is None: dirtyVnodeList = []
# Set types & helpers
bunch.kind = 'delete'
bunch.undoType = command
# Set helpers
bunch.undoHelper = u.undoDeleteNode
bunch.redoHelper = u.redoDeleteNode
bunch.newP = p.copy()
bunch.dirtyVnodeList = dirtyVnodeList
bunch.newChanged = c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
u.pushBead(bunch)
#@+node:ekr.20111005152227.15555: *5* u.afterDeleteMarkedNodes
[docs] def afterDeleteMarkedNodes(self, data, p):
u = self; c = u.c
if u.redoing or u.undoing: return
bunch = u.createCommonBunch(p)
# Set types & helpers
bunch.kind = 'delete-marked-nodes'
bunch.undoType = 'delete-marked-nodes'
# Set helpers
bunch.undoHelper = u.undoDeleteMarkedNodes
bunch.redoHelper = u.redoDeleteMarkedNodes
bunch.newP = p.copy()
bunch.deleteMarkedNodesData = data
# bunch.dirtyVnodeList = dirtyVnodeList
bunch.newChanged = c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
u.pushBead(bunch)
#@+node:ekr.20080425060424.8: *5* u.afterDemote
[docs] def afterDemote(self, p, followingSibs, dirtyVnodeList):
'''Create an undo node for demote operations.'''
u = self
bunch = u.createCommonBunch(p)
# Set types.
bunch.kind = 'demote'
bunch.undoType = 'Demote'
bunch.undoHelper = u.undoDemote
bunch.redoHelper = u.redoDemote
bunch.followingSibs = followingSibs
# Push the bunch.
u.bead += 1
u.beads[u.bead:] = [bunch]
# Recalculate the menu labels.
u.setUndoTypes()
#@+node:ekr.20050411193627.7: *5* u.afterHoist
[docs] def afterHoist(self, p, command):
u = self
if u.redoing or u.undoing:
return
bunch = u.createCommonBunch(p)
# Set types & helpers
bunch.kind = 'hoist'
bunch.undoType = command
# Set helpers
bunch.undoHelper = u.undoHoistNode
bunch.redoHelper = u.redoHoistNode
u.pushBead(bunch)
#@+node:ekr.20050411193627.9: *5* u.afterInsertNode
[docs] def afterInsertNode(self, p, command, bunch, dirtyVnodeList=None):
u = self; c = u.c
if u.redoing or u.undoing:
return
if dirtyVnodeList is None: dirtyVnodeList = []
# Set types & helpers
bunch.kind = 'insert'
bunch.undoType = command
# Set helpers
bunch.undoHelper = u.undoInsertNode
bunch.redoHelper = u.redoInsertNode
bunch.newP = p.copy()
bunch.dirtyVnodeList = dirtyVnodeList
bunch.newBack = p.back()
bunch.newParent = p.parent()
bunch.newChanged = c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
if bunch.pasteAsClone:
beforeTree = bunch.beforeTree
afterTree = []
for bunch2 in beforeTree:
v = bunch2.v
afterTree.append(
g.Bunch(v=v, head=v.h[:], body=v.b[:]))
bunch.afterTree = afterTree
u.pushBead(bunch)
#@+node:ekr.20050526124257: *5* u.afterMark
[docs] def afterMark(self, p, command, bunch, dirtyVnodeList=None):
'''Create an undo node for mark and unmark commands.'''
# 'command' unused, but present for compatibility with similar methods.
u = self
if u.redoing or u.undoing: return
if dirtyVnodeList is None: dirtyVnodeList = []
# Set the type & helpers.
bunch.undoHelper = u.undoMark
bunch.redoHelper = u.redoMark
bunch.dirtyVnodeList = dirtyVnodeList
bunch.newChanged = u.c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
u.pushBead(bunch)
#@+node:ekr.20050410110343: *5* u.afterMoveNode
[docs] def afterMoveNode(self, p, command, bunch, dirtyVnodeList=None):
u = self; c = u.c
if u.redoing or u.undoing: return
if dirtyVnodeList is None: dirtyVnodeList = []
# Set the types & helpers.
bunch.kind = 'move'
bunch.undoType = command
# Set helper only for undo:
# The bead pointer will point to an 'beforeGroup' bead for redo.
bunch.undoHelper = u.undoMove
bunch.redoHelper = u.redoMove
bunch.dirtyVnodeList = dirtyVnodeList
bunch.newChanged = c.isChanged()
bunch.newDirty = p.isDirty()
bunch.newMarked = p.isMarked()
bunch.newN = p.childIndex()
bunch.newParent_v = p._parentVnode()
bunch.newP = p.copy()
u.pushBead(bunch)
#@+node:ekr.20080425060424.12: *5* u.afterPromote
#@+node:ekr.20080425060424.2: *5* u.afterSort
[docs] def afterSort(self, p, bunch, dirtyVnodeList):
'''Create an undo node for sort operations'''
u = self
# c = self.c
if u.redoing or u.undoing: return
bunch.dirtyVnodeList = dirtyVnodeList
# Recalculate the menu labels.
u.setUndoTypes()
#@+node:ekr.20050318085432.3: *4* u.beforeX...
#@+node:ekr.20050315134017.7: *5* u.beforeChangeGroup
[docs] def beforeChangeGroup(self, p, command, verboseUndoGroup=True):
'''Prepare to undo a group of undoable operations.'''
u = self
bunch = u.createCommonBunch(p)
# Set types.
bunch.kind = 'beforeGroup'
bunch.undoType = command
bunch.verboseUndoGroup = verboseUndoGroup
# Set helper only for redo:
# The bead pointer will point to an 'afterGroup' bead for undo.
bunch.undoHelper = u.undoGroup
bunch.redoHelper = u.redoGroup
bunch.items = []
# Push the bunch.
u.bead += 1
u.beads[u.bead:] = [bunch]
#@+node:ekr.20050315133212.2: *5* u.beforeChangeNodeContents
[docs] def beforeChangeNodeContents(self, p, oldBody=None, oldHead=None, oldYScroll=None):
'''Return data that gets passed to afterChangeNode'''
u = self
bunch = u.createCommonBunch(p)
bunch.oldBody = oldBody or p.b
bunch.oldHead = oldHead or p.h
bunch.oldYScroll = oldYScroll
return bunch
#@+node:ekr.20050315134017.6: *5* u.beforeChangeTree
[docs] def beforeChangeTree(self, p):
u = self; c = u.c
w = c.frame.body.wrapper
bunch = u.createCommonBunch(p)
bunch.oldSel = w.getSelectionRange()
bunch.oldText = w.getAllText()
bunch.oldTree = u.saveTree(p)
return bunch
#@+node:ekr.20050424161505.1: *5* u.beforeClearRecentFiles
[docs] def beforeClearRecentFiles(self):
u = self; p = u.c.p
bunch = u.createCommonBunch(p)
bunch.oldRecentFiles = g.app.config.recentFiles[:]
return bunch
#@+node:ekr.20050412080354: *5* u.beforeCloneNode
[docs] def beforeCloneNode(self, p):
u = self
bunch = u.createCommonBunch(p)
return bunch
#@+node:ekr.20050411193627.3: *5* u.beforeDeleteNode
[docs] def beforeDeleteNode(self, p):
u = self
bunch = u.createCommonBunch(p)
bunch.oldBack = p.back()
bunch.oldParent = p.parent()
return bunch
#@+node:ekr.20050411193627.4: *5* u.beforeInsertNode
[docs] def beforeInsertNode(self, p, pasteAsClone=False, copiedBunchList=None):
u = self
if copiedBunchList is None: copiedBunchList = []
bunch = u.createCommonBunch(p)
bunch.pasteAsClone = pasteAsClone
if pasteAsClone:
# Save the list of bunched.
bunch.beforeTree = copiedBunchList
return bunch
#@+node:ekr.20050526131252: *5* u.beforeMark
#@+node:ekr.20050410110215: *5* u.beforeMoveNode
[docs] def beforeMoveNode(self, p):
u = self
bunch = u.createCommonBunch(p)
bunch.oldN = p.childIndex()
bunch.oldParent_v = p._parentVnode()
return bunch
#@+node:ekr.20080425060424.3: *5* u.beforeSort
[docs] def beforeSort(self, p, undoType, oldChildren, newChildren, sortChildren):
'''Create an undo node for sort operations.'''
u = self
bunch = u.createCommonBunch(p)
# Set types.
bunch.kind = 'sort'
bunch.undoType = undoType
bunch.undoHelper = u.undoSort
bunch.redoHelper = u.redoSort
bunch.oldChildren = oldChildren
bunch.newChildren = newChildren
bunch.sortChildren = sortChildren # A bool
# Push the bunch.
u.bead += 1
u.beads[u.bead:] = [bunch]
return bunch
#@+node:ekr.20050318085432.2: *5* u.createCommonBunch
[docs] def createCommonBunch(self, p):
'''Return a bunch containing all common undo info.
This is mostly the info for recreating an empty node at position p.'''
u = self; c = u.c; w = c.frame.body.wrapper
return g.Bunch(
oldChanged=c.isChanged(),
oldDirty=p and p.isDirty(),
oldMarked=p and p.isMarked(),
oldSel=w and w.getSelectionRange() or None,
p=p and p.copy(),
)
#@+node:ekr.20031218072017.3610: *4* u.canRedo & canUndo
# Translation does not affect these routines.
[docs] def canRedo(self):
u = self
return u.redoMenuLabel != "Can't Redo"
[docs] def canUndo(self):
u = self
return u.undoMenuLabel != "Can't Undo"
#@+node:ekr.20031218072017.3609: *4* u.clearUndoState
[docs] def clearUndoState(self):
"""Clears then entire Undo state.
All non-undoable commands should call this method."""
u = self
u.clearOptionalIvars() # Do this first.
u.setRedoType("Can't Redo")
u.setUndoType("Can't Undo")
u.beads = [] # List of undo nodes.
u.bead = -1 # Index of the present bead: -1:len(beads)
#@+node:ekr.20031218072017.3611: *4* u.enableMenuItems
#@+node:ekr.20110519074734.6094: *4* u.onSelect & helpers
[docs] def onSelect(self, old_p, p):
u = self
if u.per_node_undo:
if old_p and u.beads:
u.putIvarsToVnode(old_p)
u.setIvarsFromVnode(p)
u.setUndoTypes()
#@+node:ekr.20110519074734.6096: *5* u.putIvarsToVnode
[docs] def putIvarsToVnode(self, p):
u = self; v = p.v
assert self.per_node_undo
bunch = g.bunch()
for key in self.optionalIvars:
bunch[key] = getattr(u, key)
# Put these ivars by hand.
for key in ('bead', 'beads', 'undoType',):
bunch[key] = getattr(u, key)
v.undo_info = bunch
#@+node:ekr.20110519074734.6095: *5* u.setIvarsFromVnode
[docs] def setIvarsFromVnode(self, p):
u = self; v = p.v
assert self.per_node_undo
u.clearUndoState()
if hasattr(v, 'undo_info'):
u.setIvarsFromBunch(v.undo_info)
#@+node:ekr.20031218072017.1490: *4* u.setUndoTypingParams
[docs] def setUndoTypingParams(self, p, undo_type, oldText, newText,
oldSel=None, newSel=None, oldYview=None,
):
'''
Save enough information to undo or redo typing operation.
Do nothing when called from the undo/redo logic because the Undo
and Redo commands merely reset the bead pointer.
'''
u = self; c = u.c
#@+<< return if there is nothing to do >>
#@+node:ekr.20040324061854: *5* << return if there is nothing to do >>
if u.redoing or u.undoing:
return None
if undo_type is None:
return None
if undo_type == "Can't Undo":
u.clearUndoState()
u.setUndoTypes() # Must still recalculate the menu labels.
return None
if oldText == newText:
u.setUndoTypes() # Must still recalculate the menu labels.
return None
#@-<< return if there is nothing to do >>
#@+<< init the undo params >>
#@+node:ekr.20040324061854.1: *5* << init the undo params >>
# Clear all optional params.
# for ivar in u.optionalIvars:
# setattr(u,ivar,None)
u.clearOptionalIvars()
# Set the params.
u.undoType = undo_type
u.p = p.copy()
#@-<< init the undo params >>
#@+<< compute leading, middle & trailing lines >>
#@+node:ekr.20031218072017.1491: *5* << compute leading, middle & trailing lines >>
#@+at Incremental undo typing is similar to incremental syntax coloring. We compute
# the number of leading and trailing lines that match, and save both the old and
# new middle lines. NB: the number of old and new middle lines may be different.
#@@c
old_lines = oldText.split('\n')
new_lines = newText.split('\n')
new_len = len(new_lines)
old_len = len(old_lines)
min_len = min(old_len, new_len)
i = 0
while i < min_len:
if old_lines[i] != new_lines[i]:
break
i += 1
leading = i
if leading == new_len:
# This happens when we remove lines from the end.
# The new text is simply the leading lines from the old text.
trailing = 0
else:
i = 0
while i < min_len - leading:
if old_lines[old_len - i - 1] != new_lines[new_len - i - 1]:
break
i += 1
trailing = i
# NB: the number of old and new middle lines may be different.
if trailing == 0:
old_middle_lines = old_lines[leading:]
new_middle_lines = new_lines[leading:]
else:
old_middle_lines = old_lines[leading: -trailing]
new_middle_lines = new_lines[leading: -trailing]
# Remember how many trailing newlines in the old and new text.
i = len(oldText) - 1; old_newlines = 0
while i >= 0 and oldText[i] == '\n':
old_newlines += 1
i -= 1
i = len(newText) - 1; new_newlines = 0
while i >= 0 and newText[i] == '\n':
new_newlines += 1
i -= 1
#@-<< compute leading, middle & trailing lines >>
#@+<< save undo text info >>
#@+node:ekr.20031218072017.1492: *5* << save undo text info >>
#@+at This is the start of the incremental undo algorithm.
#
# We must save enough info to do _both_ of the following:
#
# Undo: Given newText, recreate oldText.
# Redo: Given oldText, recreate oldText.
#
# The "given" texts for the undo and redo routines are simply p.b.
#@@c
if u.debug_Undoer:
# Remember the complete text for comparisons...
u.oldText = oldText
u.newText = newText
# Compute statistics comparing old and new ways...
# The old doesn't often store the old text, so don't count it here.
u.old_mem += len(newText)
s1 = '\n'.join(old_middle_lines)
s2 = '\n'.join(new_middle_lines)
u.new_mem += len(s1) + len(s2)
else:
u.oldText = None
u.newText = None
u.leading = leading
u.trailing = trailing
u.oldMiddleLines = old_middle_lines
u.newMiddleLines = new_middle_lines
u.oldNewlines = old_newlines
u.newNewlines = new_newlines
#@-<< save undo text info >>
#@+<< save the selection and scrolling position >>
#@+node:ekr.20040324061854.2: *5* << save the selection and scrolling position >>
# Remember the selection.
u.oldSel = oldSel
u.newSel = newSel
# Remember the scrolling position.
if oldYview:
u.yview = oldYview
else:
u.yview = c.frame.body.wrapper.getYScrollPosition()
#@-<< save the selection and scrolling position >>
#@+<< adjust the undo stack, clearing all forward entries >>
#@+node:ekr.20040324061854.3: *5* << adjust the undo stack, clearing all forward entries >>
#@+at New in Leo 4.3. Instead of creating a new bead on every character, we
# may adjust the top bead:
#
# word granularity: adjust the top bead if the typing would continue the word.
# line granularity: adjust the top bead if the typing is on the same line.
# node granularity: adjust the top bead if the typing is anywhere on the same node.
#@@c
granularity = u.granularity
old_d = u.peekBead(u.bead)
old_p = old_d and old_d.get('p')
#@+<< set newBead if we can't share the previous bead >>
#@+node:ekr.20050125220613: *6* << set newBead if we can't share the previous bead >>
#@+at We must set newBead to True if undo_type is not 'Typing' so that commands that
# get treated like typing (by updateBodyPane and onBodyChanged) don't get lumped
# with 'real' typing.
#@@c
if (
not old_d or not old_p or
old_p.v != p.v or
old_d.get('kind') != 'typing' or
old_d.get('undoType') != 'Typing' or
undo_type != 'Typing'
):
newBead = True # We can't share the previous node.
elif granularity == 'char':
newBead = True # This was the old way.
elif granularity == 'node':
newBead = False # Always replace previous bead.
else:
assert granularity in ('line', 'word')
# Replace the previous bead if only the middle lines have changed.
newBead = (
old_d.get('leading', 0) != u.leading or
old_d.get('trailing', 0) != u.trailing
)
if granularity == 'word' and not newBead:
# Protect the method that may be changed by the user
try:
#@+<< set newBead if the change does not continue a word >>
#@+node:ekr.20050125203937: *7* << set newBead if the change does not continue a word >>
# Fix #653: undoer problem: be wary of the ternary operator here.
old_start = old_end = new_start = new_end = 0
if oldSel:
old_start, old_end = oldSel
if newSel:
new_start, new_end = newSel
prev_start, prev_end = u.prevSel
if old_start != old_end or new_start != new_end:
# The new and old characters are not contiguous.
newBead = True
else:
# 2011/04/01: Patch by Sam Hartsfield
old_row, old_col = g.convertPythonIndexToRowCol(oldText, old_start)
new_row, new_col = g.convertPythonIndexToRowCol(newText, new_start)
prev_row, prev_col = g.convertPythonIndexToRowCol(oldText, prev_start)
old_lines = g.splitLines(oldText)
new_lines = g.splitLines(newText)
# Recognize backspace, del, etc. as contiguous.
if old_row != new_row or abs(old_col - new_col) != 1:
# The new and old characters are not contiguous.
newBead = True
elif old_col == 0 or new_col == 0:
# py-lint: disable=W0511
# W0511:1362: TODO
# TODO this is not true, we might as well just have entered a
# char at the beginning of an existing line
pass # We have just inserted a line.
else:
# 2011/04/01: Patch by Sam Hartsfield
old_s = old_lines[old_row]
new_s = new_lines[new_row]
# New in 4.3b2:
# Guard against invalid oldSel or newSel params.
if old_col - 1 >= len(old_s) or new_col - 1 >= len(new_s):
newBead = True
else:
old_ch = old_s[old_col - 1]
new_ch = new_s[new_col - 1]
newBead = self.recognizeStartOfTypingWord(
old_lines, old_row, old_col, old_ch,
new_lines, new_row, new_col, new_ch,
prev_row, prev_col)
#@-<< set newBead if the change does not continue a word >>
except Exception:
g.error('Unexpected exception...')
g.es_exception()
newBead = True
#@-<< set newBead if we can't share the previous bead >>
# Save end selection as new "previous" selection
u.prevSel = u.newSel
if newBead:
# Push params on undo stack, clearing all forward entries.
bunch = g.Bunch(
p=p.copy(),
kind='typing',
undoType=undo_type,
undoHelper=u.undoTyping,
redoHelper=u.redoTyping,
oldText=u.oldText,
oldSel=u.oldSel,
oldNewlines=u.oldNewlines,
oldMiddleLines=u.oldMiddleLines,
)
u.pushBead(bunch)
else:
bunch = old_d
bunch.dirtyVnodeList = p.setAllAncestorAtFileNodesDirty()
# Bug fix: Leo 4.4.6: always add p to the list.
bunch.dirtyVnodeList.append(p.copy())
bunch.leading = u.leading
bunch.trailing = u.trailing
bunch.newNewlines = u.newNewlines
bunch.newMiddleLines = u.newMiddleLines
bunch.newSel = u.newSel
bunch.newText = u.newText
bunch.yview = u.yview
#@-<< adjust the undo stack, clearing all forward entries >>
if u.per_node_undo:
u.putIvarsToVnode(p)
return bunch # Never used.
#@+node:ekr.20031218072017.2030: *3* u.redo
[docs] @cmd('redo')
def redo(self, event=None):
'''Redo the operation undone by the last undo.'''
u = self; c = u.c
w = c.frame.body.wrapper
if not c.p:
return
# End editing *before* getting state.
c.endEditing()
if not u.canRedo():
return
if not u.getBead(u.bead + 1):
return
u.redoing = True
u.groupCount = 0
if u.redoHelper:
u.redoHelper()
else:
g.trace('no redo helper for %s %s' % (u.kind, u.undoType))
c.checkOutline()
# Redraw and recolor.
c.frame.body.updateEditors() # New in Leo 4.4.8.
if 0: # Don't do this: it interferes with selection ranges.
# This strange code forces a recomputation of the root position.
c.selectPosition(c.p)
else:
c.setCurrentPosition(c.p)
if u.newChanged is None: u.newChanged = True
c.setChanged(u.newChanged)
# Redrawing *must* be done here before setting u.undoing to False.
i, j = w.getSelectionRange()
ins = w.getInsertPoint()
c.redraw()
c.recolor()
if u.inHead: # 2013/08/26.
c.editHeadline()
u.inHead = False
else:
c.bodyWantsFocus()
w.setSelectionRange(i, j, insert=ins)
w.seeInsertPoint()
u.redoing = False
u.bead += 1
u.setUndoTypes()
#@+node:ekr.20110519074734.6092: *3* u.redo helpers
#@+node:ekr.20050424170219: *4* u.redoClearRecentFiles
[docs] def redoClearRecentFiles(self):
u = self; c = u.c
rf = g.app.recentFilesManager
rf.setRecentFiles(u.newRecentFiles[:])
rf.createRecentFilesMenuItems(c)
#@+node:ekr.20111005152227.15558: *4* u.redoCloneMarkedNodes
[docs] def redoCloneMarkedNodes(self):
u = self; c = u.c
c.selectPosition(u.p)
c.cloneMarked()
u.newP = c.p
u.newChanged = c.isChanged()
#@+node:ekr.20160502175557.1: *4* u.redoCopyMarkedNodes
[docs] def redoCopyMarkedNodes(self):
u = self; c = u.c
c.selectPosition(u.p)
c.copyMarked()
u.newP = c.p
u.newChanged = c.isChanged()
#@+node:ekr.20050412083057: *4* u.redoCloneNode
[docs] def redoCloneNode(self):
u = self; c = u.c; cc = c.chapterController
if cc: cc.selectChapterByName('main')
if u.newBack:
u.newP._linkAfter(u.newBack)
elif u.newParent:
u.newP._linkAsNthChild(u.newParent, 0)
else:
oldRoot = c.rootPosition()
u.newP._linkAsRoot(oldRoot)
for v in u.dirtyVnodeList:
v.setDirty()
c.selectPosition(u.newP)
#@+node:ekr.20111005152227.15559: *4* u.redoDeleteMarkedNodes
[docs] def redoDeleteMarkedNodes(self):
u = self; c = u.c
c.selectPosition(u.p)
c.deleteMarked()
c.selectPosition(u.newP)
u.newChanged = c.isChanged()
#@+node:EKR.20040526072519.2: *4* u.redoDeleteNode
[docs] def redoDeleteNode(self):
u = self; c = u.c
c.selectPosition(u.p)
c.deleteOutline()
c.selectPosition(u.newP)
#@+node:ekr.20080425060424.9: *4* u.redoDemote
[docs] def redoDemote(self):
u = self; c = u.c
parent_v = u.p._parentVnode()
n = u.p.childIndex()
# Move the demoted nodes from the old parent to the new parent.
parent_v.children = parent_v.children[: n + 1]
u.p.v.children.extend(u.followingSibs)
# Adjust the parent links of the moved nodes.
# There is no need to adjust descendant links.
for v in u.followingSibs:
v.parents.remove(parent_v)
v.parents.append(u.p.v)
c.setCurrentPosition(u.p)
#@+node:ekr.20050318085432.6: *4* u.redoGroup
[docs] def redoGroup(self):
'''Process beads until the matching 'afterGroup' bead is seen.'''
u = self
# Remember these values.
c = u.c
dirtyVnodeList = u.dirtyVnodeList or []
newSel = u.newSel
p = u.p.copy()
u.groupCount += 1
bunch = u.beads[u.bead]; count = 0
if not hasattr(bunch, 'items'):
g.trace('oops: expecting bunch.items. bunch.kind = %s' % bunch.kind)
g.trace(bunch)
else:
for z in bunch.items:
self.setIvarsFromBunch(z)
if z.redoHelper:
z.redoHelper(); count += 1
else:
g.trace('oops: no redo helper for %s %s' % (u.undoType, p.h))
u.groupCount -= 1
u.updateMarks('new') # Bug fix: Leo 4.4.6.
for v in dirtyVnodeList:
v.setDirty()
if not g.unitTesting and u.verboseUndoGroup:
g.es("redo", count, "instances")
c.selectPosition(p)
if newSel:
i, j = newSel
c.frame.body.wrapper.setSelectionRange(i, j)
#@+node:ekr.20050412085138.1: *4* u.redoHoistNode & redoDehoistNode
[docs] def redoHoistNode(self):
u = self; c = u.c
c.selectPosition(u.p)
c.hoist()
[docs] def redoDehoistNode(self):
u = self; c = u.c
c.selectPosition(u.p)
c.dehoist()
#@+node:ekr.20050412084532: *4* u.redoInsertNode
[docs] def redoInsertNode(self):
u = self; c = u.c; cc = c.chapterController
if cc:
cc.selectChapterByName('main')
if u.newBack:
u.newP._linkAfter(u.newBack)
elif u.newParent:
u.newP._linkAsNthChild(u.newParent, 0)
else:
oldRoot = c.rootPosition()
u.newP._linkAsRoot(oldRoot)
if u.pasteAsClone:
for bunch in u.afterTree:
v = bunch.v
if u.newP.v == v:
c.setBodyString(u.newP, bunch.body)
c.setHeadString(u.newP, bunch.head)
else:
v.setBodyString(bunch.body)
v.setHeadString(bunch.head)
c.selectPosition(u.newP)
#@+node:ekr.20050526125801: *4* u.redoMark
[docs] def redoMark(self):
u = self; c = u.c
u.updateMarks('new')
if u.groupCount == 0:
for v in u.dirtyVnodeList:
v.setDirty()
c.selectPosition(u.p)
#@+node:ekr.20050411111847: *4* u.redoMove
[docs] def redoMove(self):
u = self; c = u.c; cc = c.chapterController
v = u.p.v
assert(u.oldParent_v)
assert(u.newParent_v)
assert(v)
if cc: cc.selectChapterByName('main')
# Adjust the children arrays.
assert u.oldParent_v.children[u.oldN] == v
del u.oldParent_v.children[u.oldN]
parent_v = u.newParent_v
parent_v.children.insert(u.newN, v)
v.parents.append(u.newParent_v)
v.parents.remove(u.oldParent_v)
u.updateMarks('new')
for v in u.dirtyVnodeList:
v.setDirty()
c.selectPosition(u.newP)
#@+node:ekr.20050318085432.7: *4* u.redoNodeContents
[docs] def redoNodeContents(self):
u = self; c = u.c; w = c.frame.body.wrapper
# Restore the body.
u.p.setBodyString(u.newBody)
w.setAllText(u.newBody)
c.frame.body.recolor(u.p)
# Restore the headline.
u.p.initHeadString(u.newHead)
# This is required so. Otherwise redraw will revert the change!
c.frame.tree.setHeadline(u.p, u.newHead) # New in 4.4b2.
if u.groupCount == 0 and u.newSel:
i, j = u.newSel
w.setSelectionRange(i, j)
if u.groupCount == 0 and u.newYScroll is not None:
w.setYScrollPosition(u.newYScroll)
u.updateMarks('new')
for v in u.dirtyVnodeList:
v.setDirty()
#@+node:ekr.20080425060424.13: *4* u.redoPromote
#@+node:ekr.20080425060424.4: *4* u.redoSort
[docs] def redoSort(self):
u = self; c = u.c
parent_v = u.p._parentVnode()
parent_v.children = u.newChildren
p = c.setPositionAfterSort(u.sortChildren)
c.setCurrentPosition(p)
#@+node:ekr.20050318085432.8: *4* u.redoTree
[docs] def redoTree(self):
'''Redo replacement of an entire tree.'''
u = self; c = u.c
u.p = self.undoRedoTree(u.p, u.oldTree, u.newTree)
c.selectPosition(u.p) # Does full recolor.
if u.newSel:
i, j = u.newSel
c.frame.body.wrapper.setSelectionRange(i, j)
#@+node:EKR.20040526075238.5: *4* u.redoTyping
[docs] def redoTyping(self):
u = self; c = u.c; current = c.p
w = c.frame.body.wrapper
# selectPosition causes recoloring, so avoid if possible.
if current != u.p:
c.selectPosition(u.p)
self.undoRedoText(
u.p, u.leading, u.trailing,
u.newMiddleLines, u.oldMiddleLines,
u.newNewlines, u.oldNewlines,
tag="redo", undoType=u.undoType)
u.updateMarks('new')
for v in u.dirtyVnodeList:
v.setDirty()
if u.newSel:
c.bodyWantsFocus()
i, j = u.newSel
w.setSelectionRange(i, j, insert=j)
if u.yview:
c.bodyWantsFocus()
w.setYScrollPosition(u.yview)
#@+node:ekr.20031218072017.2039: *3* u.undo
[docs] @cmd('undo')
def undo(self, event=None):
"""Undo the operation described by the undo parameters."""
u = self; c = u.c
w = c.frame.body.wrapper
if not c.p:
return g.trace('no current position')
# End editing *before* getting state.
c.endEditing()
if u.per_node_undo: # 2011/05/19
u.setIvarsFromVnode(c.p)
if not u.canUndo():
return
if not u.getBead(u.bead):
return
u.undoing = True
u.groupCount = 0
if u.undoHelper:
u.undoHelper()
else:
g.trace('no undo helper for %s %s' % (u.kind, u.undoType))
c.checkOutline()
# Redraw and recolor.
c.frame.body.updateEditors() # New in Leo 4.4.8.
if 0: # Don't do this: it interferes with selection ranges.
# This strange code forces a recomputation of the root position.
c.selectPosition(c.p)
else:
c.setCurrentPosition(c.p)
if u.oldChanged is None: u.oldChanged = True
c.setChanged(u.oldChanged)
# Redrawing *must* be done here before setting u.undoing to False.
i, j = w.getSelectionRange()
ins = w.getInsertPoint()
c.redraw()
c.recolor()
if u.inHead:
c.editHeadline()
u.inHead = False
else:
c.bodyWantsFocus()
w.setSelectionRange(i, j, insert=ins)
w.seeInsertPoint()
u.undoing = False
u.bead -= 1
u.setUndoTypes()
#@+node:ekr.20110519074734.6093: *3* u.undo helpers
#@+node:ekr.20050424170219.1: *4* u.undoClearRecentFiles
[docs] def undoClearRecentFiles(self):
u = self; c = u.c
rf = g.app.recentFilesManager
rf.setRecentFiles(u.oldRecentFiles[:])
rf.createRecentFilesMenuItems(c)
#@+node:ekr.20111005152227.15560: *4* u.undoCloneMarkedNodes
[docs] def undoCloneMarkedNodes(self):
u = self
next = u.p.next()
assert next.h == 'Clones of marked nodes', (u.p, next.h)
next.doDelete()
u.p.setAllAncestorAtFileNodesDirty()
u.c.selectPosition(u.p)
#@+node:ekr.20160502175653.1: *4* u.undoCopyMarkedNodes
[docs] def undoCopyMarkedNodes(self):
u = self
next = u.p.next()
assert next.h == 'Copies of marked nodes', (u.p.h, next.h)
next.doDelete()
u.p.setAllAncestorAtFileNodesDirty()
u.c.selectPosition(u.p)
#@+node:ekr.20050412083057.1: *4* u.undoCloneNode
[docs] def undoCloneNode(self):
u = self; c = u.c; cc = c.chapterController
if cc: cc.selectChapterByName('main')
c.selectPosition(u.newP)
c.deleteOutline()
for v in u.dirtyVnodeList:
v.setDirty() # Bug fix: Leo 4.4.6
c.selectPosition(u.p)
#@+node:ekr.20111005152227.15557: *4* u.undoDeleteMarkedNodes
[docs] def undoDeleteMarkedNodes(self):
u = self; c = u.c
# Undo the deletes in reverse order
aList = u.deleteMarkedNodesData[:]
aList.reverse()
for p in aList:
if p.stack:
parent_v, junk = p.stack[-1]
else:
parent_v = c.hiddenRootNode
p.v._addLink(p._childIndex, parent_v)
u.p.setAllAncestorAtFileNodesDirty()
c.selectPosition(u.p)
#@+node:ekr.20050412084055: *4* u.undoDeleteNode
[docs] def undoDeleteNode(self):
u = self; c = u.c
if u.oldBack:
u.p._linkAfter(u.oldBack)
elif u.oldParent:
u.p._linkAsNthChild(u.oldParent, 0)
else:
oldRoot = c.rootPosition()
u.p._linkAsRoot(oldRoot)
u.p.setAllAncestorAtFileNodesDirty()
c.selectPosition(u.p)
#@+node:ekr.20080425060424.10: *4* u.undoDemote
[docs] def undoDemote(self):
u = self; c = u.c
parent_v = u.p._parentVnode()
n = len(u.followingSibs)
# Remove the demoted nodes from p's children.
u.p.v.children = u.p.v.children[: -n]
# Add the demoted nodes to the parent's children.
parent_v.children.extend(u.followingSibs)
# Adjust the parent links.
# There is no need to adjust descendant links.
for sib in u.followingSibs:
sib.parents.remove(u.p.v)
sib.parents.append(parent_v)
c.setCurrentPosition(u.p)
#@+node:ekr.20050318085713: *4* u.undoGroup
[docs] def undoGroup(self):
'''Process beads until the matching 'beforeGroup' bead is seen.'''
u = self
# Remember these values.
c = u.c
dirtyVnodeList = u.dirtyVnodeList or []
oldSel = u.oldSel
p = u.p.copy()
u.groupCount += 1
bunch = u.beads[u.bead]; count = 0
if not hasattr(bunch, 'items'):
g.trace('oops: expecting bunch.items. bunch.kind = %s' % bunch.kind)
g.trace(bunch)
else:
# Important bug fix: 9/8/06: reverse the items first.
reversedItems = bunch.items[:]
reversedItems.reverse()
for z in reversedItems:
self.setIvarsFromBunch(z)
if z.undoHelper:
z.undoHelper(); count += 1
else:
g.trace('oops: no undo helper for %s %s' % (u.undoType, p.v))
u.groupCount -= 1
u.updateMarks('old') # Bug fix: Leo 4.4.6.
for v in dirtyVnodeList:
v.setDirty() # Bug fix: Leo 4.4.6.
if not g.unitTesting and u.verboseUndoGroup:
g.es("undo", count, "instances")
c.selectPosition(p)
if oldSel:
i, j = oldSel
c.frame.body.wrapper.setSelectionRange(i, j)
#@+node:ekr.20050412083244: *4* u.undoHoistNode & undoDehoistNode
[docs] def undoHoistNode(self):
u = self; c = u.c
c.selectPosition(u.p)
c.dehoist()
[docs] def undoDehoistNode(self):
u = self; c = u.c
c.selectPosition(u.p)
c.hoist()
#@+node:ekr.20050412085112: *4* u.undoInsertNode
[docs] def undoInsertNode(self):
u = self; c = u.c; cc = c.chapterController
if cc: cc.selectChapterByName('main')
c.selectPosition(u.newP)
c.deleteOutline()
# Bug fix: 2016/03/30.
# This always selects the proper new position.
# c.selectPosition(u.p)
if u.pasteAsClone:
for bunch in u.beforeTree:
v = bunch.v
if u.p.v == v:
c.setBodyString(u.p, bunch.body)
c.setHeadString(u.p, bunch.head)
else:
v.setBodyString(bunch.body)
v.setHeadString(bunch.head)
#@+node:ekr.20050526124906: *4* u.undoMark
[docs] def undoMark(self):
u = self; c = u.c
u.updateMarks('old')
if u.groupCount == 0:
for v in u.dirtyVnodeList:
v.setDirty() # Bug fix: Leo 4.4.6.
c.selectPosition(u.p)
#@+node:ekr.20050411112033: *4* u.undoMove
[docs] def undoMove(self):
u = self; c = u.c; cc = c.chapterController
if cc: cc.selectChapterByName('main')
v = u.p.v
assert(u.oldParent_v)
assert(u.newParent_v)
assert(v)
# Adjust the children arrays.
assert u.newParent_v.children[u.newN] == v
del u.newParent_v.children[u.newN]
u.oldParent_v.children.insert(u.oldN, v)
# Recompute the parent links.
v.parents.append(u.oldParent_v)
v.parents.remove(u.newParent_v)
u.updateMarks('old')
for v in u.dirtyVnodeList:
v.setDirty()
c.selectPosition(u.p)
#@+node:ekr.20050318085713.1: *4* u.undoNodeContents
[docs] def undoNodeContents(self):
'''Undo all changes to the contents of a node,
including headline and body text, and marked bits.
'''
u = self; c = u.c
w = c.frame.body.wrapper
u.p.b = u.oldBody
w.setAllText(u.oldBody)
c.frame.body.recolor(u.p)
u.p.h = u.oldHead
# This is required. Otherwise c.redraw will revert the change!
c.frame.tree.setHeadline(u.p, u.oldHead)
if u.groupCount == 0 and u.oldSel:
i, j = u.oldSel
w.setSelectionRange(i, j)
if u.groupCount == 0 and u.oldYScroll is not None:
w.setYScrollPosition(u.oldYScroll)
u.updateMarks('old')
for v in u.dirtyVnodeList:
v.setDirty() # Bug fix: Leo 4.4.6.
#@+node:ekr.20080425060424.14: *4* u.undoPromote
#@+node:ekr.20031218072017.1493: *4* u.undoRedoText
[docs] def undoRedoText(self, p,
leading, trailing, # Number of matching leading & trailing lines.
oldMidLines, newMidLines, # Lists of unmatched lines.
oldNewlines, newNewlines, # Number of trailing newlines.
tag="undo", # "undo" or "redo"
undoType=None
):
'''Handle text undo and redo: converts _new_ text into _old_ text.'''
# newNewlines is unused, but it has symmetry.
u = self; c = u.c; w = c.frame.body.wrapper
#@+<< Compute the result using p's body text >>
#@+node:ekr.20061106105812.1: *5* << Compute the result using p's body text >>
# Recreate the text using the present body text.
body = p.b
body = g.toUnicode(body)
body_lines = body.split('\n')
s = []
if leading > 0:
s.extend(body_lines[: leading])
if oldMidLines:
s.extend(oldMidLines)
if trailing > 0:
s.extend(body_lines[-trailing:])
s = '\n'.join(s)
# Remove trailing newlines in s.
while s and s[-1] == '\n':
s = s[: -1]
# Add oldNewlines newlines.
if oldNewlines > 0:
s = s + '\n' * oldNewlines
result = s
if u.debug_print:
g.pr("body: ", body)
g.pr("result:", result)
#@-<< Compute the result using p's body text >>
p.setBodyString(result)
w.setAllText(result)
sel = u.oldSel if tag == 'undo' else u.newSel
if sel:
i, j = sel
w.setSelectionRange(i, j, insert=j)
c.frame.body.recolor(p)
w.seeInsertPoint() # 2009/12/21
#@+node:ekr.20050408100042: *4* u.undoRedoTree
[docs] def undoRedoTree(self, p, new_data, old_data):
'''Replace p and its subtree using old_data during undo.'''
# Same as undoReplace except uses g.Bunch.
u = self; c = u.c
if new_data is None:
# This is the first time we have undone the operation.
# Put the new data in the bead.
bunch = u.beads[u.bead]
bunch.newTree = u.saveTree(p.copy())
u.beads[u.bead] = bunch
# Replace data in tree with old data.
u.restoreTree(old_data)
c.setBodyString(p, p.b)
return p # Nothing really changes.
#@+node:ekr.20080425060424.5: *4* u.undoSort
[docs] def undoSort(self):
u = self; c = u.c
parent_v = u.p._parentVnode()
parent_v.children = u.oldChildren
p = c.setPositionAfterSort(u.sortChildren)
c.setCurrentPosition(p)
#@+node:ekr.20050318085713.2: *4* u.undoTree
[docs] def undoTree(self):
'''Redo replacement of an entire tree.'''
u = self; c = u.c
u.p = self.undoRedoTree(u.p, u.newTree, u.oldTree)
c.selectPosition(u.p) # Does full recolor.
if u.oldSel:
i, j = u.oldSel
c.frame.body.wrapper.setSelectionRange(i, j)
#@+node:EKR.20040526090701.4: *4* u.undoTyping
[docs] def undoTyping(self):
u = self; c = u.c; current = c.p
w = c.frame.body.wrapper
# selectPosition causes recoloring, so don't do this unless needed.
if current != u.p:
c.selectPosition(u.p)
self.undoRedoText(
u.p, u.leading, u.trailing,
u.oldMiddleLines, u.newMiddleLines,
u.oldNewlines, u.newNewlines,
tag="undo", undoType=u.undoType)
u.updateMarks('old')
for v in u.dirtyVnodeList:
v.setDirty() # Bug fix: Leo 4.4.6.
if u.oldSel:
c.bodyWantsFocus()
i, j = u.oldSel
w.setSelectionRange(i, j, insert=j)
if u.yview:
c.bodyWantsFocus()
w.setYScrollPosition(u.yview)
#@-others
#@-others
#@@language python
#@@tabwidth -4
#@@pagewidth 70
#@-leo