Coverage for C:\Repos\leo-editor\leo\core\leoUndo.py: 72%
1333 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
1#@+leo-ver=5-thin
2#@+node:ekr.20031218072017.3603: * @file leoUndo.py
3# Suppress all mypy errors (mypy doesn't like g.Bunch).
4# type: ignore
5"""Leo's undo/redo manager."""
6#@+<< How Leo implements unlimited undo >>
7#@+node:ekr.20031218072017.2413: ** << How Leo implements unlimited undo >>
8#@@language rest
9#@+at
10# Think of the actions that may be Undone or Redone as a string of beads
11# (g.Bunches) containing all information needed to undo _and_ redo an operation.
12#
13# A bead pointer points to the present bead. Undoing an operation moves the bead
14# pointer backwards; redoing an operation moves the bead pointer forwards. The
15# bead pointer points in front of the first bead when Undo is disabled. The bead
16# pointer points at the last bead when Redo is disabled.
17#
18# The Undo command uses the present bead to undo the action, then moves the bead
19# pointer backwards. The Redo command uses the bead after the present bead to redo
20# the action, then moves the bead pointer forwards. The list of beads does not
21# branch; all undoable operations (except the Undo and Redo commands themselves)
22# delete any beads following the newly created bead.
23#
24# New in Leo 4.3: User (client) code should call u.beforeX and u.afterX methods to
25# create a bead describing the operation that is being performed. (By convention,
26# the code sets u = c.undoer for undoable operations.) Most u.beforeX methods
27# return 'undoData' that the client code merely passes to the corresponding
28# u.afterX method. This data contains the 'before' snapshot. The u.afterX methods
29# then create a bead containing both the 'before' and 'after' snapshots.
30#
31# New in Leo 4.3: u.beforeChangeGroup and u.afterChangeGroup allow multiple calls
32# to u.beforeX and u.afterX methods to be treated as a single undoable entry. See
33# the code for the Replace All, Sort, Promote and Demote commands for examples.
34# u.before/afterChangeGroup substantially reduce the number of u.before/afterX
35# methods needed.
36#
37# New in Leo 4.3: It would be possible for plugins or other code to define their
38# own u.before/afterX methods. Indeed, u.afterX merely needs to set the
39# bunch.undoHelper and bunch.redoHelper ivars to the methods used to undo and redo
40# the operation. See the code for the various u.before/afterX methods for
41# guidance.
42#
43# I first saw this model of unlimited undo in the documentation for Apple's Yellow Box classes.
44#@-<< How Leo implements unlimited undo >>
45from leo.core import leoGlobals as g
46# pylint: disable=unpacking-non-sequence
47#@+others
48#@+node:ekr.20150509193222.1: ** u.cmd (decorator)
49def cmd(name):
50 """Command decorator for the Undoer class."""
51 return g.new_cmd_decorator(name, ['c', 'undoer',])
52#@+node:ekr.20031218072017.3605: ** class Undoer
53class Undoer:
54 """A class that implements unlimited undo and redo."""
55 # pylint: disable=not-an-iterable
56 # pylint: disable=unsubscriptable-object
57 # So that ivars can be inited to None rather thatn [].
58 #@+others
59 #@+node:ekr.20150509193307.1: *3* u.Birth
60 #@+node:ekr.20031218072017.3606: *4* u.__init__
61 def __init__(self, c):
62 self.c = c
63 self.granularity = None # Set in reloadSettings.
64 self.max_undo_stack_size = c.config.getInt('max-undo-stack-size') or 0
65 # State ivars...
66 self.beads = [] # List of undo nodes.
67 self.bead = -1 # Index of the present bead: -1:len(beads)
68 self.undoType = "Can't Undo"
69 # These must be set here, _not_ in clearUndoState.
70 self.redoMenuLabel = "Can't Redo"
71 self.undoMenuLabel = "Can't Undo"
72 self.realRedoMenuLabel = "Can't Redo"
73 self.realUndoMenuLabel = "Can't Undo"
74 self.undoing = False # True if executing an Undo command.
75 self.redoing = False # True if executing a Redo command.
76 self.per_node_undo = False # True: v may contain undo_info ivar.
77 # New in 4.2...
78 self.optionalIvars = []
79 # Set the following ivars to keep pylint happy.
80 self.afterTree = None
81 self.beforeTree = None
82 self.children = None
83 self.deleteMarkedNodesData = None
84 self.followingSibs = None
85 self.inHead = None
86 self.kind = None
87 self.newBack = None
88 self.newBody = None
89 self.newChildren = None
90 self.newHead = None
91 self.newIns = None
92 self.newMarked = None
93 self.newN = None
94 self.newP = None
95 self.newParent = None
96 self.newParent_v = None
97 self.newRecentFiles = None
98 self.newSel = None
99 self.newTree = None
100 self.newYScroll = None
101 self.oldBack = None
102 self.oldBody = None
103 self.oldChildren = None
104 self.oldHead = None
105 self.oldIns = None
106 self.oldMarked = None
107 self.oldN = None
108 self.oldParent = None
109 self.oldParent_v = None
110 self.oldRecentFiles = None
111 self.oldSel = None
112 self.oldTree = None
113 self.oldYScroll = None
114 self.pasteAsClone = None
115 self.prevSel = None
116 self.sortChildren = None
117 self.verboseUndoGroup = None
118 self.reloadSettings()
119 #@+node:ekr.20191213085126.1: *4* u.reloadSettings
120 def reloadSettings(self):
121 """Undoer.reloadSettings."""
122 c = self.c
123 self.granularity = c.config.getString('undo-granularity')
124 if self.granularity:
125 self.granularity = self.granularity.lower()
126 if self.granularity not in ('node', 'line', 'word', 'char'):
127 self.granularity = 'line'
128 #@+node:ekr.20050416092908.1: *3* u.Internal helpers
129 #@+node:ekr.20031218072017.3607: *4* u.clearOptionalIvars
130 def clearOptionalIvars(self):
131 u = self
132 u.p = None # The position/node being operated upon for undo and redo.
133 for ivar in u.optionalIvars:
134 setattr(u, ivar, None)
135 #@+node:ekr.20060127052111.1: *4* u.cutStack
136 def cutStack(self):
137 u = self
138 n = u.max_undo_stack_size
139 if u.bead >= n > 0 and not g.unitTesting:
140 # Do nothing if we are in the middle of creating a group.
141 i = len(u.beads) - 1
142 while i >= 0:
143 bunch = u.beads[i]
144 if hasattr(bunch, 'kind') and bunch.kind == 'beforeGroup':
145 return
146 i -= 1
147 # This work regardless of how many items appear after bead n.
148 # g.trace('Cutting undo stack to %d entries' % (n))
149 u.beads = u.beads[-n :]
150 u.bead = n - 1
151 if 'undo' in g.app.debug and 'verbose' in g.app.debug: # pragma: no cover
152 print(f"u.cutStack: {len(u.beads):3}")
153 #@+node:ekr.20080623083646.10: *4* u.dumpBead
154 def dumpBead(self, n): # pragma: no cover
155 u = self
156 if n < 0 or n >= len(u.beads):
157 return 'no bead: n = ', n
158 # bunch = u.beads[n]
159 result = []
160 result.append('-' * 10)
161 result.append(f"len(u.beads): {len(u.beads)}, n: {n}")
162 for ivar in ('kind', 'newP', 'newN', 'p', 'oldN', 'undoHelper'):
163 result.append(f"{ivar} = {getattr(self, ivar)}")
164 return '\n'.join(result)
166 def dumpTopBead(self): # pragma: no cover
167 u = self
168 n = len(u.beads)
169 if n > 0:
170 return self.dumpBead(n - 1)
171 return '<no top bead>'
172 #@+node:EKR.20040526150818: *4* u.getBead
173 def getBead(self, n):
174 """Set Undoer ivars from the bunch at the top of the undo stack."""
175 u = self
176 if n < 0 or n >= len(u.beads):
177 return None # pragma: no cover
178 bunch = u.beads[n]
179 self.setIvarsFromBunch(bunch)
180 if 'undo' in g.app.debug: # pragma: no cover
181 print(f" u.getBead: {n:3} of {len(u.beads)}")
182 return bunch
183 #@+node:EKR.20040526150818.1: *4* u.peekBead
184 def peekBead(self, n):
186 u = self
187 if n < 0 or n >= len(u.beads):
188 return None
189 return u.beads[n]
190 #@+node:ekr.20060127113243: *4* u.pushBead
191 def pushBead(self, bunch):
192 u = self
193 # New in 4.4b2: Add this to the group if it is being accumulated.
194 bunch2 = u.bead >= 0 and u.bead < len(u.beads) and u.beads[u.bead]
195 if bunch2 and hasattr(bunch2, 'kind') and bunch2.kind == 'beforeGroup':
196 # Just append the new bunch the group's items.
197 bunch2.items.append(bunch)
198 else:
199 # Push the bunch.
200 u.bead += 1
201 u.beads[u.bead:] = [bunch]
202 # Recalculate the menu labels.
203 u.setUndoTypes()
204 if 'undo' in g.app.debug: # pragma: no cover
205 print(f"u.pushBead: {len(u.beads):3} {bunch.undoType}")
206 #@+node:ekr.20031218072017.3613: *4* u.redoMenuName, undoMenuName
207 def redoMenuName(self, name):
208 if name == "Can't Redo":
209 return name
210 return "Redo " + name
212 def undoMenuName(self, name):
213 if name == "Can't Undo":
214 return name
215 return "Undo " + name
216 #@+node:ekr.20060127070008: *4* u.setIvarsFromBunch
217 def setIvarsFromBunch(self, bunch):
218 u = self
219 u.clearOptionalIvars()
220 if False and not g.unitTesting: # Debugging. # pragma: no cover
221 print('-' * 40)
222 for key in list(bunch.keys()):
223 g.trace(f"{key:20} {bunch.get(key)!r}")
224 print('-' * 20)
225 if g.unitTesting: # #1694: An ever-present unit test.
226 val = bunch.get('oldMarked')
227 assert val in (True, False), f"{val!r} {g.callers()!s}"
228 # bunch is not a dict, so bunch.keys() is required.
229 for key in list(bunch.keys()):
230 val = bunch.get(key)
231 setattr(u, key, val)
232 if key not in u.optionalIvars:
233 u.optionalIvars.append(key)
234 #@+node:ekr.20031218072017.3614: *4* u.setRedoType
235 # These routines update both the ivar and the menu label.
237 def setRedoType(self, theType):
239 u = self
240 frame = u.c.frame
241 if not isinstance(theType, str): # pragma: no cover
242 g.trace(f"oops: expected string for command, got {theType!r}")
243 g.trace(g.callers())
244 theType = '<unknown>'
245 menu = frame.menu.getMenu("Edit")
246 name = u.redoMenuName(theType)
247 if name != u.redoMenuLabel:
248 # Update menu using old name.
249 realLabel = frame.menu.getRealMenuName(name)
250 if realLabel == name:
251 underline = -1 if g.match(name, 0, "Can't") else 0
252 else:
253 underline = realLabel.find("&")
254 realLabel = realLabel.replace("&", "")
255 frame.menu.setMenuLabel(
256 menu, u.realRedoMenuLabel, realLabel, underline=underline)
257 u.redoMenuLabel = name
258 u.realRedoMenuLabel = realLabel
259 #@+node:ekr.20091221145433.6381: *4* u.setUndoType
260 def setUndoType(self, theType):
262 u = self
263 frame = u.c.frame
264 if not isinstance(theType, str):
265 g.trace(f"oops: expected string for command, got {repr(theType)}")
266 g.trace(g.callers())
267 theType = '<unknown>'
268 menu = frame.menu.getMenu("Edit")
269 name = u.undoMenuName(theType)
270 if name != u.undoMenuLabel:
271 # Update menu using old name.
272 realLabel = frame.menu.getRealMenuName(name)
273 if realLabel == name:
274 underline = -1 if g.match(name, 0, "Can't") else 0
275 else:
276 underline = realLabel.find("&")
277 realLabel = realLabel.replace("&", "")
278 frame.menu.setMenuLabel(
279 menu, u.realUndoMenuLabel, realLabel, underline=underline)
280 u.undoType = theType
281 u.undoMenuLabel = name
282 u.realUndoMenuLabel = realLabel
283 #@+node:ekr.20031218072017.3616: *4* u.setUndoTypes
284 def setUndoTypes(self):
286 u = self
287 # Set the undo type and undo menu label.
288 bunch = u.peekBead(u.bead)
289 if bunch:
290 u.setUndoType(bunch.undoType)
291 else:
292 u.setUndoType("Can't Undo")
293 # Set only the redo menu label.
294 bunch = u.peekBead(u.bead + 1)
295 if bunch:
296 u.setRedoType(bunch.undoType)
297 else:
298 u.setRedoType("Can't Redo")
299 u.cutStack()
300 #@+node:EKR.20040530121329: *4* u.restoreTree & helpers
301 def restoreTree(self, treeInfo):
302 """Use the tree info to restore all VNode data,
303 including all links."""
304 u = self
305 # This effectively relinks all vnodes.
306 for v, vInfo in treeInfo:
307 u.restoreVnodeUndoInfo(vInfo)
308 #@+node:ekr.20050415170737.2: *5* u.restoreVnodeUndoInfo
309 def restoreVnodeUndoInfo(self, bunch):
310 """Restore all ivars saved in the bunch."""
311 v = bunch.v
312 v.statusBits = bunch.statusBits
313 v.children = bunch.children
314 v.parents = bunch.parents
315 uA = bunch.get('unknownAttributes')
316 if uA is not None:
317 v.unknownAttributes = uA
318 v._p_changed = True
319 #@+node:ekr.20050415170812.2: *5* u.restoreTnodeUndoInfo
320 def restoreTnodeUndoInfo(self, bunch):
321 v = bunch.v
322 v.h = bunch.headString
323 v.b = bunch.bodyString
324 v.statusBits = bunch.statusBits
325 uA = bunch.get('unknownAttributes')
326 if uA is not None:
327 v.unknownAttributes = uA
328 v._p_changed = True
329 #@+node:EKR.20040528075307: *4* u.saveTree & helpers
330 def saveTree(self, p, treeInfo=None):
331 """Return a list of tuples with all info needed to handle a general undo operation."""
332 # WARNING: read this before doing anything "clever"
333 #@+<< about u.saveTree >>
334 #@+node:EKR.20040530114124: *5* << about u.saveTree >>
335 #@@language rest
336 #@+at
337 # The old code made a free-standing copy of the tree using v.copy and
338 # t.copy. This looks "elegant" and is WRONG. The problem is that it can
339 # not handle clones properly, especially when some clones were in the
340 # "undo" tree and some were not. Moreover, it required complex
341 # adjustments to t.vnodeLists.
342 #
343 # Instead of creating new nodes, the new code creates all information needed
344 # to properly restore the vnodes. It creates a list of tuples, on tuple for
345 # each VNode in the tree. Each tuple has the form (v, vnodeInfo), where
346 # vnodeInfo is a dict containing all info needed to recreate the nodes. The
347 # v.createUndoInfoDict method corresponds to the old v.copy method.
348 #
349 # Aside: Prior to 4.2 Leo used a scheme that was equivalent to the
350 # createUndoInfoDict info, but quite a bit uglier.
351 #@-<< about u.saveTree >>
352 u = self
353 topLevel = (treeInfo is None)
354 if topLevel:
355 treeInfo = []
356 # Add info for p.v. Duplicate info is harmless.
357 data = (p.v, u.createVnodeUndoInfo(p.v))
358 treeInfo.append(data)
359 # Recursively add info for the subtree.
360 child = p.firstChild()
361 while child:
362 self.saveTree(child, treeInfo)
363 child = child.next()
364 return treeInfo
365 #@+node:ekr.20050415170737.1: *5* u.createVnodeUndoInfo
366 def createVnodeUndoInfo(self, v):
367 """Create a bunch containing all info needed to recreate a VNode for undo."""
368 bunch = g.Bunch(
369 v=v,
370 statusBits=v.statusBits,
371 parents=v.parents[:],
372 children=v.children[:],
373 )
374 if hasattr(v, 'unknownAttributes'):
375 bunch.unknownAttributes = v.unknownAttributes
376 return bunch
377 #@+node:ekr.20050525151449: *4* u.trace
378 def trace(self): # pragma: no cover
379 ivars = ('kind', 'undoType')
380 for ivar in ivars:
381 g.pr(ivar, getattr(self, ivar))
382 #@+node:ekr.20050410095424: *4* u.updateMarks
383 def updateMarks(self, oldOrNew):
384 """Update dirty and marked bits."""
385 u = self
386 c = u.c
387 if oldOrNew not in ('new', 'old'): # pragma: no cover
388 g.trace("can't happen")
389 return
390 isOld = oldOrNew == 'old'
391 marked = u.oldMarked if isOld else u.newMarked
392 # Note: c.set/clearMarked call a hook.
393 if marked:
394 c.setMarked(u.p)
395 else:
396 c.clearMarked(u.p)
397 # Undo/redo always set changed/dirty bits because the file may have been saved.
398 u.p.setDirty()
399 u.c.setChanged()
400 #@+node:ekr.20031218072017.3608: *3* u.Externally visible entries
401 #@+node:ekr.20050318085432.4: *4* u.afterX...
402 #@+node:ekr.20201109075104.1: *5* u.afterChangeBody
403 def afterChangeBody(self, p, command, bunch):
404 """
405 Create an undo node using d created by beforeChangeNode.
407 *Important*: Before calling this method, caller must:
408 - Set p.v.b. (Setting p.b would cause a redraw).
409 - Set the desired selection range and insert point.
410 - Set the y-scroll position, if desired.
411 """
412 c = self.c
413 u, w = self, c.frame.body.wrapper
414 if u.redoing or u.undoing:
415 return # pragma: no cover
416 # Set the type & helpers.
417 bunch.kind = 'body'
418 bunch.undoType = command
419 bunch.undoHelper = u.undoChangeBody
420 bunch.redoHelper = u.redoChangeBody
421 bunch.newBody = p.b
422 bunch.newHead = p.h
423 bunch.newIns = w.getInsertPoint()
424 bunch.newMarked = p.isMarked()
425 # Careful: don't use ternary operator.
426 if w:
427 bunch.newSel = w.getSelectionRange()
428 else:
429 bunch.newSel = 0, 0 # pragma: no cover
430 bunch.newYScroll = w.getYScrollPosition() if w else 0
431 u.pushBead(bunch)
432 #
433 if g.unitTesting:
434 assert command.lower() != 'typing', g.callers()
435 elif command.lower() == 'typing': # pragma: no cover
436 g.trace(
437 'Error: undoType should not be "Typing"\n'
438 'Call u.doTyping instead')
439 u.updateAfterTyping(p, w)
440 #@+node:ekr.20050315134017.4: *5* u.afterChangeGroup
441 def afterChangeGroup(self, p, undoType, reportFlag=False):
442 """
443 Create an undo node for general tree operations using d created by
444 beforeChangeGroup
445 """
446 c, u = self.c, self
447 w = c.frame.body.wrapper
448 if p != c.p: # Prepare to ignore p argument.
449 if not u.changeGroupWarning:
450 u.changeGroupWarning = True
451 g.trace("Position mismatch", g.callers())
452 if u.redoing or u.undoing:
453 return # pragma: no cover
454 bunch = u.beads[u.bead]
455 if not u.beads: # pragma: no cover
456 g.trace('oops: empty undo stack.')
457 return
458 if bunch.kind == 'beforeGroup':
459 bunch.kind = 'afterGroup'
460 else: # pragma: no cover
461 g.trace(f"oops: expecting beforeGroup, got {bunch.kind}")
462 # Set the types & helpers.
463 bunch.kind = 'afterGroup'
464 bunch.undoType = undoType
465 # Set helper only for undo:
466 # The bead pointer will point to an 'beforeGroup' bead for redo.
467 bunch.undoHelper = u.undoGroup
468 bunch.redoHelper = u.redoGroup
469 bunch.newP = p.copy()
470 bunch.newSel = w.getSelectionRange()
471 # Tells whether to report the number of separate changes undone/redone.
472 bunch.reportFlag = reportFlag
473 if 0:
474 # Push the bunch.
475 u.bead += 1
476 u.beads[u.bead:] = [bunch]
477 # Recalculate the menu labels.
478 u.setUndoTypes()
479 #@+node:ekr.20050315134017.2: *5* u.afterChangeNodeContents
480 def afterChangeNodeContents(self, p, command, bunch):
481 """Create an undo node using d created by beforeChangeNode."""
482 u = self
483 c = self.c
484 w = c.frame.body.wrapper
485 if u.redoing or u.undoing:
486 return
487 # Set the type & helpers.
488 bunch.kind = 'node'
489 bunch.undoType = command
490 bunch.undoHelper = u.undoNodeContents
491 bunch.redoHelper = u.redoNodeContents
492 bunch.inHead = False # 2013/08/26
493 bunch.newBody = p.b
494 bunch.newHead = p.h
495 bunch.newMarked = p.isMarked()
496 # Bug fix 2017/11/12: don't use ternary operator.
497 if w:
498 bunch.newSel = w.getSelectionRange()
499 else:
500 bunch.newSel = 0, 0 # pragma: no cover
501 bunch.newYScroll = w.getYScrollPosition() if w else 0
502 u.pushBead(bunch)
503 #@+node:ekr.20201107145642.1: *5* u.afterChangeHeadline
504 def afterChangeHeadline(self, p, command, bunch):
505 """Create an undo node using d created by beforeChangeHeadline."""
506 u = self
507 if u.redoing or u.undoing:
508 return # pragma: no cover
509 # Set the type & helpers.
510 bunch.kind = 'headline'
511 bunch.undoType = command
512 bunch.undoHelper = u.undoChangeHeadline
513 bunch.redoHelper = u.redoChangeHeadline
514 bunch.newHead = p.h
515 u.pushBead(bunch)
517 afterChangeHead = afterChangeHeadline
518 #@+node:ekr.20050315134017.3: *5* u.afterChangeTree
519 def afterChangeTree(self, p, command, bunch):
520 """Create an undo node for general tree operations using d created by beforeChangeTree"""
521 u = self
522 c = self.c
523 w = c.frame.body.wrapper
524 if u.redoing or u.undoing:
525 return # pragma: no cover
526 # Set the types & helpers.
527 bunch.kind = 'tree'
528 bunch.undoType = command
529 bunch.undoHelper = u.undoTree
530 bunch.redoHelper = u.redoTree
531 # Set by beforeChangeTree: changed, oldSel, oldText, oldTree, p
532 bunch.newSel = w.getSelectionRange()
533 bunch.newText = w.getAllText()
534 bunch.newTree = u.saveTree(p)
535 u.pushBead(bunch)
536 #@+node:ekr.20050424161505: *5* u.afterClearRecentFiles
537 def afterClearRecentFiles(self, bunch):
538 u = self
539 bunch.newRecentFiles = g.app.config.recentFiles[:]
540 bunch.undoType = 'Clear Recent Files'
541 bunch.undoHelper = u.undoClearRecentFiles
542 bunch.redoHelper = u.redoClearRecentFiles
543 u.pushBead(bunch)
544 return bunch
545 #@+node:ekr.20111006060936.15639: *5* u.afterCloneMarkedNodes
546 def afterCloneMarkedNodes(self, p):
547 u = self
548 if u.redoing or u.undoing:
549 return
550 # createCommonBunch sets:
551 # oldDirty = p.isDirty()
552 # oldMarked = p.isMarked()
553 # oldSel = w and w.getSelectionRange() or None
554 # p = p.copy()
555 bunch = u.createCommonBunch(p)
556 # Set types.
557 bunch.kind = 'clone-marked-nodes'
558 bunch.undoType = 'clone-marked-nodes'
559 # Set helpers.
560 bunch.undoHelper = u.undoCloneMarkedNodes
561 bunch.redoHelper = u.redoCloneMarkedNodes
562 bunch.newP = p.next()
563 bunch.newMarked = p.isMarked()
564 u.pushBead(bunch)
565 #@+node:ekr.20160502175451.1: *5* u.afterCopyMarkedNodes
566 def afterCopyMarkedNodes(self, p):
567 u = self
568 if u.redoing or u.undoing:
569 return
570 # createCommonBunch sets:
571 # oldDirty = p.isDirty()
572 # oldMarked = p.isMarked()
573 # oldSel = w and w.getSelectionRange() or None
574 # p = p.copy()
575 bunch = u.createCommonBunch(p)
576 # Set types.
577 bunch.kind = 'copy-marked-nodes'
578 bunch.undoType = 'copy-marked-nodes'
579 # Set helpers.
580 bunch.undoHelper = u.undoCopyMarkedNodes
581 bunch.redoHelper = u.redoCopyMarkedNodes
582 bunch.newP = p.next()
583 bunch.newMarked = p.isMarked()
584 u.pushBead(bunch)
585 #@+node:ekr.20050411193627.5: *5* u.afterCloneNode
586 def afterCloneNode(self, p, command, bunch):
587 u = self
588 if u.redoing or u.undoing:
589 return # pragma: no cover
590 # Set types & helpers
591 bunch.kind = 'clone'
592 bunch.undoType = command
593 # Set helpers
594 bunch.undoHelper = u.undoCloneNode
595 bunch.redoHelper = u.redoCloneNode
596 bunch.newBack = p.back() # 6/15/05
597 bunch.newParent = p.parent() # 6/15/05
598 bunch.newP = p.copy()
599 bunch.newMarked = p.isMarked()
600 u.pushBead(bunch)
601 #@+node:ekr.20050411193627.6: *5* u.afterDehoist
602 def afterDehoist(self, p, command):
603 u = self
604 if u.redoing or u.undoing:
605 return
606 bunch = u.createCommonBunch(p)
607 # Set types & helpers
608 bunch.kind = 'dehoist'
609 bunch.undoType = command
610 # Set helpers
611 bunch.undoHelper = u.undoDehoistNode
612 bunch.redoHelper = u.redoDehoistNode
613 u.pushBead(bunch)
614 #@+node:ekr.20050411193627.8: *5* u.afterDeleteNode
615 def afterDeleteNode(self, p, command, bunch):
616 u = self
617 if u.redoing or u.undoing:
618 return
619 # Set types & helpers
620 bunch.kind = 'delete'
621 bunch.undoType = command
622 # Set helpers
623 bunch.undoHelper = u.undoDeleteNode
624 bunch.redoHelper = u.redoDeleteNode
625 bunch.newP = p.copy()
626 bunch.newMarked = p.isMarked()
627 u.pushBead(bunch)
628 #@+node:ekr.20111005152227.15555: *5* u.afterDeleteMarkedNodes
629 def afterDeleteMarkedNodes(self, data, p):
630 u = self
631 if u.redoing or u.undoing:
632 return
633 bunch = u.createCommonBunch(p)
634 # Set types & helpers
635 bunch.kind = 'delete-marked-nodes'
636 bunch.undoType = 'delete-marked-nodes'
637 # Set helpers
638 bunch.undoHelper = u.undoDeleteMarkedNodes
639 bunch.redoHelper = u.redoDeleteMarkedNodes
640 bunch.newP = p.copy()
641 bunch.deleteMarkedNodesData = data
642 bunch.newMarked = p.isMarked()
643 u.pushBead(bunch)
644 #@+node:ekr.20080425060424.8: *5* u.afterDemote
645 def afterDemote(self, p, followingSibs):
646 """Create an undo node for demote operations."""
647 u = self
648 bunch = u.createCommonBunch(p)
649 # Set types.
650 bunch.kind = 'demote'
651 bunch.undoType = 'Demote'
652 bunch.undoHelper = u.undoDemote
653 bunch.redoHelper = u.redoDemote
654 bunch.followingSibs = followingSibs
655 # Push the bunch.
656 u.bead += 1
657 u.beads[u.bead:] = [bunch]
658 # Recalculate the menu labels.
659 u.setUndoTypes()
660 #@+node:ekr.20050411193627.7: *5* u.afterHoist
661 def afterHoist(self, p, command):
662 u = self
663 if u.redoing or u.undoing:
664 return # pragma: no cover
665 bunch = u.createCommonBunch(p)
666 # Set types & helpers
667 bunch.kind = 'hoist'
668 bunch.undoType = command
669 # Set helpers
670 bunch.undoHelper = u.undoHoistNode
671 bunch.redoHelper = u.redoHoistNode
672 u.pushBead(bunch)
673 #@+node:ekr.20050411193627.9: *5* u.afterInsertNode
674 def afterInsertNode(self, p, command, bunch):
675 u = self
676 if u.redoing or u.undoing:
677 return
678 # Set types & helpers
679 bunch.kind = 'insert'
680 bunch.undoType = command
681 # Set helpers
682 bunch.undoHelper = u.undoInsertNode
683 bunch.redoHelper = u.redoInsertNode
684 bunch.newP = p.copy()
685 bunch.newBack = p.back()
686 bunch.newParent = p.parent()
687 bunch.newMarked = p.isMarked()
688 if bunch.pasteAsClone:
689 beforeTree = bunch.beforeTree
690 afterTree = []
691 for bunch2 in beforeTree:
692 v = bunch2.v
693 afterTree.append(g.Bunch(v=v, head=v.h[:], body=v.b[:]))
694 bunch.afterTree = afterTree
695 u.pushBead(bunch)
696 #@+node:ekr.20050526124257: *5* u.afterMark
697 def afterMark(self, p, command, bunch):
698 """Create an undo node for mark and unmark commands."""
699 # 'command' unused, but present for compatibility with similar methods.
700 u = self
701 if u.redoing or u.undoing:
702 return # pragma: no cover
703 # Set the type & helpers.
704 bunch.undoHelper = u.undoMark
705 bunch.redoHelper = u.redoMark
706 bunch.newMarked = p.isMarked()
707 u.pushBead(bunch)
708 #@+node:ekr.20050410110343: *5* u.afterMoveNode
709 def afterMoveNode(self, p, command, bunch):
710 u = self
711 if u.redoing or u.undoing:
712 return
713 # Set the types & helpers.
714 bunch.kind = 'move'
715 bunch.undoType = command
716 # Set helper only for undo:
717 # The bead pointer will point to an 'beforeGroup' bead for redo.
718 bunch.undoHelper = u.undoMove
719 bunch.redoHelper = u.redoMove
720 bunch.newMarked = p.isMarked()
721 bunch.newN = p.childIndex()
722 bunch.newParent_v = p._parentVnode()
723 bunch.newP = p.copy()
724 u.pushBead(bunch)
725 #@+node:ekr.20080425060424.12: *5* u.afterPromote
726 def afterPromote(self, p, children):
727 """Create an undo node for demote operations."""
728 u = self
729 bunch = u.createCommonBunch(p)
730 # Set types.
731 bunch.kind = 'promote'
732 bunch.undoType = 'Promote'
733 bunch.undoHelper = u.undoPromote
734 bunch.redoHelper = u.redoPromote
735 bunch.children = children
736 # Push the bunch.
737 u.bead += 1
738 u.beads[u.bead:] = [bunch]
739 # Recalculate the menu labels.
740 u.setUndoTypes()
741 #@+node:ekr.20080425060424.2: *5* u.afterSort
742 def afterSort(self, p, bunch):
743 """Create an undo node for sort operations"""
744 u = self
745 # c = self.c
746 if u.redoing or u.undoing:
747 return # pragma: no cover
748 # Recalculate the menu labels.
749 u.setUndoTypes()
750 #@+node:ekr.20050318085432.3: *4* u.beforeX...
751 #@+node:ekr.20201109074740.1: *5* u.beforeChangeBody
752 def beforeChangeBody(self, p):
753 """Return data that gets passed to afterChangeBody."""
754 w = self.c.frame.body.wrapper
755 bunch = self.createCommonBunch(p) # Sets u.oldMarked, u.oldSel, u.p
756 bunch.oldBody = p.b
757 bunch.oldHead = p.h
758 bunch.oldIns = w.getInsertPoint()
759 bunch.oldYScroll = w.getYScrollPosition()
760 return bunch
761 #@+node:ekr.20050315134017.7: *5* u.beforeChangeGroup
762 changeGroupWarning = False
764 def beforeChangeGroup(self, p, command, verboseUndoGroup=True):
765 """Prepare to undo a group of undoable operations."""
766 c, u = self.c, self
767 if p != c.p: # Prepare to ignore p argument.
768 if not u.changeGroupWarning:
769 u.changeGroupWarning = True
770 g.trace("Position mismatch", g.callers())
771 bunch = u.createCommonBunch(p)
772 # Set types.
773 bunch.kind = 'beforeGroup'
774 bunch.undoType = command
775 bunch.verboseUndoGroup = verboseUndoGroup
776 # Set helper only for redo:
777 # The bead pointer will point to an 'afterGroup' bead for undo.
778 bunch.undoHelper = u.undoGroup
779 bunch.redoHelper = u.redoGroup
780 bunch.items = []
781 # Push the bunch.
782 u.bead += 1
783 u.beads[u.bead:] = [bunch]
784 #@+node:ekr.20201107145859.1: *5* u.beforeChangeHeadline
785 def beforeChangeHeadline(self, p):
786 """
787 Return data that gets passed to afterChangeNode.
789 The oldHead kwarg works around a Qt difficulty when changing headlines.
790 """
791 u = self
792 bunch = u.createCommonBunch(p)
793 bunch.oldHead = p.h
794 return bunch
796 beforeChangeHead = beforeChangeHeadline
797 #@+node:ekr.20050315133212.2: *5* u.beforeChangeNodeContents
798 def beforeChangeNodeContents(self, p):
799 """Return data that gets passed to afterChangeNode."""
800 c, u = self.c, self
801 w = c.frame.body.wrapper
802 bunch = u.createCommonBunch(p)
803 bunch.oldBody = p.b
804 bunch.oldHead = p.h
805 # #1413: Always restore yScroll if possible.
806 bunch.oldYScroll = w.getYScrollPosition() if w else 0
807 return bunch
808 #@+node:ekr.20050315134017.6: *5* u.beforeChangeTree
809 def beforeChangeTree(self, p):
810 u = self
811 c = u.c
812 w = c.frame.body.wrapper
813 bunch = u.createCommonBunch(p)
814 bunch.oldSel = w.getSelectionRange()
815 bunch.oldText = w.getAllText()
816 bunch.oldTree = u.saveTree(p)
817 return bunch
818 #@+node:ekr.20050424161505.1: *5* u.beforeClearRecentFiles
819 def beforeClearRecentFiles(self):
820 u = self
821 p = u.c.p
822 bunch = u.createCommonBunch(p)
823 bunch.oldRecentFiles = g.app.config.recentFiles[:]
824 return bunch
825 #@+node:ekr.20050412080354: *5* u.beforeCloneNode
826 def beforeCloneNode(self, p):
827 u = self
828 bunch = u.createCommonBunch(p)
829 return bunch
830 #@+node:ekr.20050411193627.3: *5* u.beforeDeleteNode
831 def beforeDeleteNode(self, p):
832 u = self
833 bunch = u.createCommonBunch(p)
834 bunch.oldBack = p.back()
835 bunch.oldParent = p.parent()
836 return bunch
837 #@+node:ekr.20050411193627.4: *5* u.beforeInsertNode
838 def beforeInsertNode(self, p, pasteAsClone=False, copiedBunchList=None):
839 u = self
840 if copiedBunchList is None:
841 copiedBunchList = []
842 bunch = u.createCommonBunch(p)
843 bunch.pasteAsClone = pasteAsClone
844 if pasteAsClone:
845 # Save the list of bunched.
846 bunch.beforeTree = copiedBunchList
847 return bunch
848 #@+node:ekr.20050526131252: *5* u.beforeMark
849 def beforeMark(self, p, command):
850 u = self
851 bunch = u.createCommonBunch(p)
852 bunch.kind = 'mark'
853 bunch.undoType = command
854 return bunch
855 #@+node:ekr.20050410110215: *5* u.beforeMoveNode
856 def beforeMoveNode(self, p):
857 u = self
858 bunch = u.createCommonBunch(p)
859 bunch.oldN = p.childIndex()
860 bunch.oldParent_v = p._parentVnode()
861 return bunch
862 #@+node:ekr.20080425060424.3: *5* u.beforeSort
863 def beforeSort(self, p, undoType, oldChildren, newChildren, sortChildren):
864 """Create an undo node for sort operations."""
865 u = self
866 bunch = u.createCommonBunch(p)
867 # Set types.
868 bunch.kind = 'sort'
869 bunch.undoType = undoType
870 bunch.undoHelper = u.undoSort
871 bunch.redoHelper = u.redoSort
872 bunch.oldChildren = oldChildren
873 bunch.newChildren = newChildren
874 bunch.sortChildren = sortChildren # A bool
875 # Push the bunch.
876 u.bead += 1
877 u.beads[u.bead:] = [bunch]
878 return bunch
879 #@+node:ekr.20050318085432.2: *5* u.createCommonBunch
880 def createCommonBunch(self, p):
881 """Return a bunch containing all common undo info.
882 This is mostly the info for recreating an empty node at position p."""
883 u = self
884 c = u.c
885 w = c.frame.body.wrapper
886 return g.Bunch(
887 oldMarked=p and p.isMarked(),
888 oldSel=w.getSelectionRange() if w else None,
889 p=p.copy() if p else None,
890 )
891 #@+node:ekr.20031218072017.3610: *4* u.canRedo & canUndo
892 # Translation does not affect these routines.
894 def canRedo(self):
895 u = self
896 return u.redoMenuLabel != "Can't Redo"
898 def canUndo(self):
899 u = self
900 return u.undoMenuLabel != "Can't Undo"
901 #@+node:ekr.20031218072017.3609: *4* u.clearUndoState
902 def clearUndoState(self):
903 """Clears then entire Undo state.
905 All non-undoable commands should call this method."""
906 u = self
907 u.clearOptionalIvars() # Do this first.
908 u.setRedoType("Can't Redo")
909 u.setUndoType("Can't Undo")
910 u.beads = [] # List of undo nodes.
911 u.bead = -1 # Index of the present bead: -1:len(beads)
912 #@+node:ekr.20031218072017.1490: *4* u.doTyping & helper
913 def doTyping(self, p, undo_type, oldText, newText,
914 newInsert=None, oldSel=None, newSel=None, oldYview=None,
915 ):
916 """
917 Save enough information to undo or redo a typing operation efficiently,
918 that is, with the proper granularity.
920 Do nothing when called from the undo/redo logic because the Undo
921 and Redo commands merely reset the bead pointer.
923 **Important**: Code should call this method *only* when the user has
924 actually typed something. Commands should use u.beforeChangeBody and
925 u.afterChangeBody.
927 Only qtm.onTextChanged and ec.selfInsertCommand now call this method.
928 """
929 c, u, w = self.c, self, self.c.frame.body.wrapper
930 # Leo 6.4: undo_type must be 'Typing'.
931 undo_type = undo_type.capitalize()
932 assert undo_type == 'Typing', (repr(undo_type), g.callers())
933 #@+<< return if there is nothing to do >>
934 #@+node:ekr.20040324061854: *5* << return if there is nothing to do >>
935 if u.redoing or u.undoing:
936 return None # pragma: no cover
937 if undo_type is None:
938 return None # pragma: no cover
939 if undo_type == "Can't Undo":
940 u.clearUndoState()
941 u.setUndoTypes() # Must still recalculate the menu labels.
942 return None # pragma: no cover
943 if oldText == newText:
944 u.setUndoTypes() # Must still recalculate the menu labels.
945 return None # pragma: no cover
946 #@-<< return if there is nothing to do >>
947 #@+<< init the undo params >>
948 #@+node:ekr.20040324061854.1: *5* << init the undo params >>
949 u.clearOptionalIvars()
950 # Set the params.
951 u.undoType = undo_type
952 u.p = p.copy()
953 #@-<< init the undo params >>
954 #@+<< compute leading, middle & trailing lines >>
955 #@+node:ekr.20031218072017.1491: *5* << compute leading, middle & trailing lines >>
956 #@+at Incremental undo typing is similar to incremental syntax coloring. We compute
957 # the number of leading and trailing lines that match, and save both the old and
958 # new middle lines. NB: the number of old and new middle lines may be different.
959 #@@c
960 old_lines = oldText.split('\n')
961 new_lines = newText.split('\n')
962 new_len = len(new_lines)
963 old_len = len(old_lines)
964 min_len = min(old_len, new_len)
965 i = 0
966 while i < min_len:
967 if old_lines[i] != new_lines[i]:
968 break
969 i += 1
970 leading = i
971 if leading == new_len:
972 # This happens when we remove lines from the end.
973 # The new text is simply the leading lines from the old text.
974 trailing = 0
975 else:
976 i = 0
977 while i < min_len - leading:
978 if old_lines[old_len - i - 1] != new_lines[new_len - i - 1]:
979 break
980 i += 1
981 trailing = i
982 # NB: the number of old and new middle lines may be different.
983 if trailing == 0:
984 old_middle_lines = old_lines[leading:]
985 new_middle_lines = new_lines[leading:]
986 else:
987 old_middle_lines = old_lines[leading : -trailing]
988 new_middle_lines = new_lines[leading : -trailing]
989 # Remember how many trailing newlines in the old and new text.
990 i = len(oldText) - 1
991 old_newlines = 0
992 while i >= 0 and oldText[i] == '\n':
993 old_newlines += 1
994 i -= 1
995 i = len(newText) - 1
996 new_newlines = 0
997 while i >= 0 and newText[i] == '\n':
998 new_newlines += 1
999 i -= 1
1000 #@-<< compute leading, middle & trailing lines >>
1001 #@+<< save undo text info >>
1002 #@+node:ekr.20031218072017.1492: *5* << save undo text info >>
1003 u.oldText = None
1004 u.newText = None
1005 u.leading = leading
1006 u.trailing = trailing
1007 u.oldMiddleLines = old_middle_lines
1008 u.newMiddleLines = new_middle_lines
1009 u.oldNewlines = old_newlines
1010 u.newNewlines = new_newlines
1011 #@-<< save undo text info >>
1012 #@+<< save the selection and scrolling position >>
1013 #@+node:ekr.20040324061854.2: *5* << save the selection and scrolling position >>
1014 # Remember the selection.
1015 u.oldSel = oldSel
1016 u.newSel = newSel
1017 # Remember the scrolling position.
1018 if oldYview:
1019 u.yview = oldYview
1020 else:
1021 u.yview = c.frame.body.wrapper.getYScrollPosition()
1022 #@-<< save the selection and scrolling position >>
1023 #@+<< adjust the undo stack, clearing all forward entries >>
1024 #@+node:ekr.20040324061854.3: *5* << adjust the undo stack, clearing all forward entries >>
1025 #@+at
1026 # New in Leo 4.3. Instead of creating a new bead on every character, we
1027 # may adjust the top bead:
1028 # word granularity: adjust the top bead if the typing would continue the word.
1029 # line granularity: adjust the top bead if the typing is on the same line.
1030 # node granularity: adjust the top bead if the typing is anywhere on the same node.
1031 #@@c
1032 granularity = u.granularity
1033 old_d = u.peekBead(u.bead)
1034 old_p = old_d and old_d.get('p')
1035 #@+<< set newBead if we can't share the previous bead >>
1036 #@+node:ekr.20050125220613: *6* << set newBead if we can't share the previous bead >>
1037 # Set newBead to True if undo_type is not 'Typing' so that commands that
1038 # get treated like typing don't get lumped with 'real' typing.
1039 if (
1040 not old_d or not old_p or
1041 old_p.v != p.v or
1042 old_d.get('kind') != 'typing' or
1043 old_d.get('undoType') != 'Typing' or
1044 undo_type != 'Typing'
1045 ):
1046 newBead = True # We can't share the previous node.
1047 elif granularity == 'char':
1048 newBead = True # This was the old way.
1049 elif granularity == 'node':
1050 newBead = False # Always replace previous bead.
1051 else:
1052 assert granularity in ('line', 'word')
1053 # Replace the previous bead if only the middle lines have changed.
1054 newBead = (
1055 old_d.get('leading', 0) != u.leading or
1056 old_d.get('trailing', 0) != u.trailing
1057 )
1058 if granularity == 'word' and not newBead:
1059 # Protect the method that may be changed by the user
1060 try:
1061 #@+<< set newBead if the change does not continue a word >>
1062 #@+node:ekr.20050125203937: *7* << set newBead if the change does not continue a word >>
1063 # Fix #653: undoer problem: be wary of the ternary operator here.
1064 old_start = old_end = new_start = new_end = 0
1065 if oldSel is not None:
1066 old_start, old_end = oldSel
1067 if newSel is not None:
1068 new_start, new_end = newSel
1069 if u.prevSel is None:
1070 prev_start, prev_end = 0, 0
1071 else:
1072 prev_start, prev_end = u.prevSel
1073 if old_start != old_end or new_start != new_end:
1074 # The new and old characters are not contiguous.
1075 newBead = True
1076 else:
1077 # 2011/04/01: Patch by Sam Hartsfield
1078 old_row, old_col = g.convertPythonIndexToRowCol(
1079 oldText, old_start)
1080 new_row, new_col = g.convertPythonIndexToRowCol(
1081 newText, new_start)
1082 prev_row, prev_col = g.convertPythonIndexToRowCol(
1083 oldText, prev_start)
1084 old_lines = g.splitLines(oldText)
1085 new_lines = g.splitLines(newText)
1086 # Recognize backspace, del, etc. as contiguous.
1087 if old_row != new_row or abs(old_col - new_col) != 1:
1088 # The new and old characters are not contiguous.
1089 newBead = True
1090 elif old_col == 0 or new_col == 0:
1091 # py-lint: disable=W0511
1092 # W0511:1362: TODO
1093 # TODO this is not true, we might as well just have entered a
1094 # char at the beginning of an existing line
1095 pass # We have just inserted a line.
1096 else:
1097 # 2011/04/01: Patch by Sam Hartsfield
1098 old_s = old_lines[old_row]
1099 new_s = new_lines[new_row]
1100 # New in 4.3b2:
1101 # Guard against invalid oldSel or newSel params.
1102 if old_col - 1 >= len(old_s) or new_col - 1 >= len(new_s):
1103 newBead = True
1104 else:
1105 old_ch = old_s[old_col - 1]
1106 new_ch = new_s[new_col - 1]
1107 newBead = self.recognizeStartOfTypingWord(
1108 old_lines, old_row, old_col, old_ch,
1109 new_lines, new_row, new_col, new_ch,
1110 prev_row, prev_col)
1111 #@-<< set newBead if the change does not continue a word >>
1112 except Exception:
1113 g.error('Unexpected exception...')
1114 g.es_exception()
1115 newBead = True
1116 #@-<< set newBead if we can't share the previous bead >>
1117 # Save end selection as new "previous" selection
1118 u.prevSel = u.newSel
1119 if newBead:
1120 # Push params on undo stack, clearing all forward entries.
1121 bunch = g.Bunch(
1122 p=p.copy(),
1123 kind='typing', # lowercase.
1124 undoType=undo_type, # capitalized.
1125 undoHelper=u.undoTyping,
1126 redoHelper=u.redoTyping,
1127 oldMarked=old_p.isMarked() if old_p else p.isMarked(), # #1694
1128 oldText=u.oldText,
1129 oldSel=u.oldSel,
1130 oldNewlines=u.oldNewlines,
1131 oldMiddleLines=u.oldMiddleLines,
1132 )
1133 u.pushBead(bunch)
1134 else:
1135 bunch = old_d
1136 bunch.leading = u.leading
1137 bunch.trailing = u.trailing
1138 bunch.newMarked = p.isMarked() # #1694
1139 bunch.newNewlines = u.newNewlines
1140 bunch.newMiddleLines = u.newMiddleLines
1141 bunch.newSel = u.newSel
1142 bunch.newText = u.newText
1143 bunch.yview = u.yview
1144 #@-<< adjust the undo stack, clearing all forward entries >>
1145 if 'undo' in g.app.debug and 'verbose' in g.app.debug:
1146 print(f"u.doTyping: {len(oldText)} => {len(newText)}")
1147 if u.per_node_undo:
1148 u.putIvarsToVnode(p)
1149 #
1150 # Finish updating the text.
1151 p.v.setBodyString(newText)
1152 u.updateAfterTyping(p, w)
1154 # Compatibility
1156 setUndoTypingParams = doTyping
1157 #@+node:ekr.20050126081529: *5* u.recognizeStartOfTypingWord
1158 def recognizeStartOfTypingWord(self,
1159 old_lines, old_row, old_col, old_ch,
1160 new_lines, new_row, new_col, new_ch,
1161 prev_row, prev_col
1162 ):
1163 """
1164 A potentially user-modifiable method that should return True if the
1165 typing indicated by the params starts a new 'word' for the purposes of
1166 undo with 'word' granularity.
1168 u.doTyping calls this method only when the typing could possibly
1169 continue a previous word. In other words, undo will work safely regardless
1170 of the value returned here.
1172 old_ch is the char at the given (Tk) row, col of old_lines.
1173 new_ch is the char at the given (Tk) row, col of new_lines.
1175 The present code uses only old_ch and new_ch. The other arguments are given
1176 for use by more sophisticated algorithms.
1177 """
1178 # Start a word if new_ch begins whitespace + word
1179 new_word_started = not old_ch.isspace() and new_ch.isspace()
1180 # Start a word if the cursor has been moved since the last change
1181 moved_cursor = new_row != prev_row or new_col != prev_col + 1
1182 return new_word_started or moved_cursor
1183 #@+node:ekr.20031218072017.3611: *4* u.enableMenuItems
1184 def enableMenuItems(self):
1185 u = self
1186 frame = u.c.frame
1187 menu = frame.menu.getMenu("Edit")
1188 if menu:
1189 frame.menu.enableMenu(menu, u.redoMenuLabel, u.canRedo())
1190 frame.menu.enableMenu(menu, u.undoMenuLabel, u.canUndo())
1191 #@+node:ekr.20110519074734.6094: *4* u.onSelect & helpers
1192 def onSelect(self, old_p, p):
1194 u = self
1195 if u.per_node_undo:
1196 if old_p and u.beads:
1197 u.putIvarsToVnode(old_p)
1198 u.setIvarsFromVnode(p)
1199 u.setUndoTypes()
1200 #@+node:ekr.20110519074734.6096: *5* u.putIvarsToVnode
1201 def putIvarsToVnode(self, p):
1203 u, v = self, p.v
1204 assert self.per_node_undo
1205 bunch = g.bunch()
1206 for key in self.optionalIvars:
1207 bunch[key] = getattr(u, key)
1208 # Put these ivars by hand.
1209 for key in ('bead', 'beads', 'undoType',):
1210 bunch[key] = getattr(u, key)
1211 v.undo_info = bunch
1212 #@+node:ekr.20110519074734.6095: *5* u.setIvarsFromVnode
1213 def setIvarsFromVnode(self, p):
1214 u = self
1215 v = p.v
1216 assert self.per_node_undo
1217 u.clearUndoState()
1218 if hasattr(v, 'undo_info'):
1219 u.setIvarsFromBunch(v.undo_info)
1220 #@+node:ekr.20201127035748.1: *4* u.updateAfterTyping
1221 def updateAfterTyping(self, p, w):
1222 """
1223 Perform all update tasks after changing body text.
1225 This is ugly, ad-hoc code, but should be done uniformly.
1226 """
1227 c = self.c
1228 if g.isTextWrapper(w):
1229 # An important, ever-present unit test.
1230 all = w.getAllText()
1231 if g.unitTesting:
1232 assert p.b == all, (w, g.callers())
1233 elif p.b != all:
1234 g.trace(
1235 f"\np.b != w.getAllText() p: {p.h} \n"
1236 f"w: {w!r} \n{g.callers()}\n")
1237 # g.printObj(g.splitLines(p.b), tag='p.b')
1238 # g.printObj(g.splitLines(all), tag='getAllText')
1239 p.v.insertSpot = ins = w.getInsertPoint()
1240 # From u.doTyping.
1241 newSel = w.getSelectionRange()
1242 if newSel is None:
1243 p.v.selectionStart, p.v.selectionLength = (ins, 0)
1244 else:
1245 i, j = newSel
1246 p.v.selectionStart, p.v.selectionLength = (i, j - i)
1247 else:
1248 if g.unitTesting:
1249 assert False, f"Not a text wrapper: {g.callers()}"
1250 g.trace('Not a text wrapper')
1251 p.v.insertSpot = 0
1252 p.v.selectionStart, p.v.selectionLength = (0, 0)
1253 #
1254 # #1749.
1255 if p.isDirty():
1256 redraw_flag = False
1257 else:
1258 p.setDirty() # Do not call p.v.setDirty!
1259 redraw_flag = True
1260 if not c.isChanged():
1261 c.setChanged()
1262 # Update editors.
1263 c.frame.body.updateEditors()
1264 # Update icons.
1265 val = p.computeIcon()
1266 if not hasattr(p.v, "iconVal") or val != p.v.iconVal:
1267 p.v.iconVal = val
1268 redraw_flag = True
1269 #
1270 # Recolor the body.
1271 c.frame.scanForTabWidth(p) # Calls frame.setTabWidth()
1272 c.recolor()
1273 if redraw_flag:
1274 c.redraw_after_icons_changed()
1275 w.setFocus()
1276 #@+node:ekr.20031218072017.2030: *3* u.redo
1277 @cmd('redo')
1278 def redo(self, event=None):
1279 """Redo the operation undone by the last undo."""
1280 c, u = self.c, self
1281 if not c.p:
1282 return
1283 # End editing *before* getting state.
1284 c.endEditing()
1285 if not u.canRedo():
1286 return
1287 if not u.getBead(u.bead + 1):
1288 return
1289 #
1290 # Init status.
1291 u.redoing = True
1292 u.groupCount = 0
1293 if u.redoHelper:
1294 u.redoHelper()
1295 else:
1296 g.trace(f"no redo helper for {u.kind} {u.undoType}")
1297 #
1298 # Finish.
1299 c.checkOutline()
1300 u.update_status()
1301 u.redoing = False
1302 u.bead += 1
1303 u.setUndoTypes()
1304 #@+node:ekr.20110519074734.6092: *3* u.redo helpers
1305 #@+node:ekr.20191213085226.1: *4* u.reloadHelper (do nothing)
1306 def redoHelper(self):
1307 """The default do-nothing redo helper."""
1308 pass
1309 #@+node:ekr.20201109080732.1: *4* u.redoChangeBody
1310 def redoChangeBody(self):
1311 c, u, w = self.c, self, self.c.frame.body.wrapper
1312 # selectPosition causes recoloring, so don't do this unless needed.
1313 if c.p != u.p: # #1333.
1314 c.selectPosition(u.p)
1315 u.p.setDirty()
1316 u.p.b = u.newBody
1317 u.p.h = u.newHead
1318 # This is required so. Otherwise redraw will revert the change!
1319 c.frame.tree.setHeadline(u.p, u.newHead)
1320 if u.newMarked:
1321 u.p.setMarked()
1322 else:
1323 u.p.clearMarked()
1324 if u.groupCount == 0:
1325 w.setAllText(u.newBody)
1326 i, j = u.newSel
1327 w.setSelectionRange(i, j, insert=u.newIns)
1328 w.setYScrollPosition(u.newYScroll)
1329 c.frame.body.recolor(u.p)
1330 u.updateMarks('new')
1331 u.p.setDirty()
1332 #@+node:ekr.20201107150619.1: *4* u.redoChangeHeadline
1333 def redoChangeHeadline(self):
1334 c, u = self.c, self
1335 # selectPosition causes recoloring, so don't do this unless needed.
1336 if c.p != u.p: # #1333.
1337 c.selectPosition(u.p)
1338 u.p.setDirty()
1339 c.frame.body.recolor(u.p)
1340 # Restore the headline.
1341 u.p.initHeadString(u.newHead)
1342 # This is required so. Otherwise redraw will revert the change!
1343 c.frame.tree.setHeadline(u.p, u.newHead)
1344 #@+node:ekr.20050424170219: *4* u.redoClearRecentFiles
1345 def redoClearRecentFiles(self):
1346 u = self
1347 c = u.c
1348 rf = g.app.recentFilesManager
1349 rf.setRecentFiles(u.newRecentFiles[:])
1350 rf.createRecentFilesMenuItems(c)
1351 #@+node:ekr.20111005152227.15558: *4* u.redoCloneMarkedNodes
1352 def redoCloneMarkedNodes(self):
1353 u = self
1354 c = u.c
1355 c.selectPosition(u.p)
1356 c.cloneMarked()
1357 u.newP = c.p
1358 #@+node:ekr.20160502175557.1: *4* u.redoCopyMarkedNodes
1359 def redoCopyMarkedNodes(self):
1360 u = self
1361 c = u.c
1362 c.selectPosition(u.p)
1363 c.copyMarked()
1364 u.newP = c.p
1365 #@+node:ekr.20050412083057: *4* u.redoCloneNode
1366 def redoCloneNode(self):
1367 u = self
1368 c = u.c
1369 cc = c.chapterController
1370 if cc:
1371 cc.selectChapterByName('main')
1372 if u.newBack:
1373 u.newP._linkAfter(u.newBack)
1374 elif u.newParent:
1375 u.newP._linkAsNthChild(u.newParent, 0)
1376 else:
1377 u.newP._linkAsRoot()
1378 c.selectPosition(u.newP)
1379 u.newP.setDirty()
1380 #@+node:ekr.20111005152227.15559: *4* u.redoDeleteMarkedNodes
1381 def redoDeleteMarkedNodes(self):
1382 u = self
1383 c = u.c
1384 c.selectPosition(u.p)
1385 c.deleteMarked()
1386 c.selectPosition(u.newP)
1387 #@+node:EKR.20040526072519.2: *4* u.redoDeleteNode
1388 def redoDeleteNode(self):
1389 u = self
1390 c = u.c
1391 c.selectPosition(u.p)
1392 c.deleteOutline()
1393 c.selectPosition(u.newP)
1394 #@+node:ekr.20080425060424.9: *4* u.redoDemote
1395 def redoDemote(self):
1396 u = self
1397 c = u.c
1398 parent_v = u.p._parentVnode()
1399 n = u.p.childIndex()
1400 # Move the demoted nodes from the old parent to the new parent.
1401 parent_v.children = parent_v.children[: n + 1]
1402 u.p.v.children.extend(u.followingSibs)
1403 # Adjust the parent links of the moved nodes.
1404 # There is no need to adjust descendant links.
1405 for v in u.followingSibs:
1406 v.parents.remove(parent_v)
1407 v.parents.append(u.p.v)
1408 u.p.setDirty()
1409 c.setCurrentPosition(u.p)
1410 #@+node:ekr.20050318085432.6: *4* u.redoGroup
1411 def redoGroup(self):
1412 """Process beads until the matching 'afterGroup' bead is seen."""
1413 c, u = self.c, self
1414 # Remember these values.
1415 newSel = u.newSel
1416 p = u.p.copy() # Exists now, but may not exist later.
1417 newP = u.newP.copy() # May not exist now, but must exist later.
1418 if g.unitTesting:
1419 assert c.positionExists(p), repr(p)
1420 u.groupCount += 1
1421 bunch = u.beads[u.bead + 1]
1422 count = 0
1423 if not hasattr(bunch, 'items'):
1424 g.trace(f"oops: expecting bunch.items. got bunch.kind = {bunch.kind}")
1425 g.trace(bunch)
1426 else:
1427 for z in bunch.items:
1428 self.setIvarsFromBunch(z)
1429 if z.redoHelper:
1430 z.redoHelper()
1431 count += 1
1432 else:
1433 g.trace(f"oops: no redo helper for {u.undoType} {p.h}")
1434 u.groupCount -= 1
1435 u.updateMarks('new') # Bug fix: Leo 4.4.6.
1436 if not g.unitTesting and u.verboseUndoGroup:
1437 g.es("redo", count, "instances")
1438 # Helpers set dirty bits.
1439 # Set c.p, independently of helpers.
1440 if g.unitTesting:
1441 assert c.positionExists(newP), repr(newP)
1442 c.selectPosition(newP)
1443 # Set the selection, independently of helpers.
1444 if newSel:
1445 i, j = newSel
1446 c.frame.body.wrapper.setSelectionRange(i, j)
1447 #@+node:ekr.20050412085138.1: *4* u.redoHoistNode & redoDehoistNode
1448 def redoHoistNode(self):
1449 c, u = self.c, self
1450 u.p.setDirty()
1451 c.selectPosition(u.p)
1452 c.hoist()
1454 def redoDehoistNode(self):
1455 c, u = self.c, self
1456 u.p.setDirty()
1457 c.selectPosition(u.p)
1458 c.dehoist()
1459 #@+node:ekr.20050412084532: *4* u.redoInsertNode
1460 def redoInsertNode(self):
1461 u = self
1462 c = u.c
1463 cc = c.chapterController
1464 if cc:
1465 cc.selectChapterByName('main')
1466 if u.newBack:
1467 u.newP._linkAfter(u.newBack)
1468 elif u.newParent:
1469 u.newP._linkAsNthChild(u.newParent, 0)
1470 else:
1471 u.newP._linkAsRoot()
1472 if u.pasteAsClone:
1473 for bunch in u.afterTree:
1474 v = bunch.v
1475 if u.newP.v == v:
1476 u.newP.b = bunch.body
1477 u.newP.h = bunch.head
1478 else:
1479 v.setBodyString(bunch.body)
1480 v.setHeadString(bunch.head)
1481 u.newP.setDirty()
1482 c.selectPosition(u.newP)
1483 #@+node:ekr.20050526125801: *4* u.redoMark
1484 def redoMark(self):
1485 u = self
1486 c = u.c
1487 u.updateMarks('new')
1488 if u.groupCount == 0:
1489 u.p.setDirty()
1490 c.selectPosition(u.p)
1491 #@+node:ekr.20050411111847: *4* u.redoMove
1492 def redoMove(self):
1493 u = self
1494 c = u.c
1495 cc = c.chapterController
1496 v = u.p.v
1497 assert u.oldParent_v
1498 assert u.newParent_v
1499 assert v
1500 if cc:
1501 cc.selectChapterByName('main')
1502 # Adjust the children arrays of the old parent.
1503 assert u.oldParent_v.children[u.oldN] == v
1504 del u.oldParent_v.children[u.oldN]
1505 u.oldParent_v.setDirty()
1506 # Adjust the children array of the new parent.
1507 parent_v = u.newParent_v
1508 parent_v.children.insert(u.newN, v)
1509 v.parents.append(u.newParent_v)
1510 v.parents.remove(u.oldParent_v)
1511 u.newParent_v.setDirty()
1512 #
1513 u.updateMarks('new')
1514 u.newP.setDirty()
1515 c.selectPosition(u.newP)
1516 #@+node:ekr.20050318085432.7: *4* u.redoNodeContents
1517 def redoNodeContents(self):
1518 c, u = self.c, self
1519 w = c.frame.body.wrapper
1520 # selectPosition causes recoloring, so don't do this unless needed.
1521 if c.p != u.p: # #1333.
1522 c.selectPosition(u.p)
1523 u.p.setDirty()
1524 # Restore the body.
1525 u.p.setBodyString(u.newBody)
1526 w.setAllText(u.newBody)
1527 c.frame.body.recolor(u.p)
1528 # Restore the headline.
1529 u.p.initHeadString(u.newHead)
1530 # This is required so. Otherwise redraw will revert the change!
1531 c.frame.tree.setHeadline(u.p, u.newHead) # New in 4.4b2.
1532 if u.groupCount == 0 and u.newSel:
1533 i, j = u.newSel
1534 w.setSelectionRange(i, j)
1535 if u.groupCount == 0 and u.newYScroll is not None:
1536 w.setYScrollPosition(u.newYScroll)
1537 u.updateMarks('new')
1538 u.p.setDirty()
1539 #@+node:ekr.20080425060424.13: *4* u.redoPromote
1540 def redoPromote(self):
1541 u = self
1542 c = u.c
1543 parent_v = u.p._parentVnode()
1544 # Add the children to parent_v's children.
1545 n = u.p.childIndex() + 1
1546 old_children = parent_v.children[:]
1547 # Add children up to the promoted nodes.
1548 parent_v.children = old_children[:n]
1549 # Add the promoted nodes.
1550 parent_v.children.extend(u.children)
1551 # Add the children up to the promoted nodes.
1552 parent_v.children.extend(old_children[n:])
1553 # Remove the old children.
1554 u.p.v.children = []
1555 # Adjust the parent links in the moved children.
1556 # There is no need to adjust descendant links.
1557 for child in u.children:
1558 child.parents.remove(u.p.v)
1559 child.parents.append(parent_v)
1560 u.p.setDirty()
1561 c.setCurrentPosition(u.p)
1562 #@+node:ekr.20080425060424.4: *4* u.redoSort
1563 def redoSort(self):
1564 u = self
1565 c = u.c
1566 parent_v = u.p._parentVnode()
1567 parent_v.children = u.newChildren
1568 p = c.setPositionAfterSort(u.sortChildren)
1569 p.setAllAncestorAtFileNodesDirty()
1570 c.setCurrentPosition(p)
1571 #@+node:ekr.20050318085432.8: *4* u.redoTree
1572 def redoTree(self):
1573 """Redo replacement of an entire tree."""
1574 c, u = self.c, self
1575 u.p = self.undoRedoTree(u.oldTree, u.newTree)
1576 u.p.setDirty()
1577 c.selectPosition(u.p) # Does full recolor.
1578 if u.newSel:
1579 i, j = u.newSel
1580 c.frame.body.wrapper.setSelectionRange(i, j)
1581 #@+node:EKR.20040526075238.5: *4* u.redoTyping
1582 def redoTyping(self):
1583 u = self
1584 c = u.c
1585 current = c.p
1586 w = c.frame.body.wrapper
1587 # selectPosition causes recoloring, so avoid if possible.
1588 if current != u.p:
1589 c.selectPosition(u.p)
1590 u.p.setDirty()
1591 self.undoRedoText(
1592 u.p, u.leading, u.trailing,
1593 u.newMiddleLines, u.oldMiddleLines,
1594 u.newNewlines, u.oldNewlines,
1595 tag="redo", undoType=u.undoType)
1596 u.updateMarks('new')
1597 if u.newSel:
1598 c.bodyWantsFocus()
1599 i, j = u.newSel
1600 w.setSelectionRange(i, j, insert=j)
1601 if u.yview:
1602 c.bodyWantsFocus()
1603 w.setYScrollPosition(u.yview)
1604 #@+node:ekr.20031218072017.2039: *3* u.undo
1605 @cmd('undo')
1606 def undo(self, event=None):
1607 """Undo the operation described by the undo parameters."""
1608 u = self
1609 c = u.c
1610 if not c.p:
1611 g.trace('no current position')
1612 return
1613 # End editing *before* getting state.
1614 c.endEditing()
1615 if u.per_node_undo: # 2011/05/19
1616 u.setIvarsFromVnode(c.p)
1617 if not u.canUndo():
1618 return
1619 if not u.getBead(u.bead):
1620 return
1621 #
1622 # Init status.
1623 u.undoing = True
1624 u.groupCount = 0
1625 #
1626 # Dispatch.
1627 if u.undoHelper:
1628 u.undoHelper()
1629 else:
1630 g.trace(f"no undo helper for {u.kind} {u.undoType}")
1631 #
1632 # Finish.
1633 c.checkOutline()
1634 u.update_status()
1635 u.undoing = False
1636 u.bead -= 1
1637 u.setUndoTypes()
1638 #@+node:ekr.20110519074734.6093: *3* u.undo helpers
1639 #@+node:ekr.20191213085246.1: *4* u.undoHelper
1640 def undoHelper(self):
1641 """The default do-nothing undo helper."""
1642 pass
1643 #@+node:ekr.20201109080631.1: *4* u.undoChangeBody
1644 def undoChangeBody(self):
1645 """
1646 Undo all changes to the contents of a node,
1647 including headline and body text, and marked bits.
1648 """
1649 c, u, w = self.c, self, self.c.frame.body.wrapper
1650 # selectPosition causes recoloring, so don't do this unless needed.
1651 if c.p != u.p:
1652 c.selectPosition(u.p)
1653 u.p.setDirty()
1654 u.p.b = u.oldBody
1655 u.p.h = u.oldHead
1656 # This is required. Otherwise c.redraw will revert the change!
1657 c.frame.tree.setHeadline(u.p, u.oldHead)
1658 if u.oldMarked:
1659 u.p.setMarked()
1660 else:
1661 u.p.clearMarked()
1662 if u.groupCount == 0:
1663 w.setAllText(u.oldBody)
1664 i, j = u.oldSel
1665 w.setSelectionRange(i, j, insert=u.oldIns)
1666 w.setYScrollPosition(u.oldYScroll)
1667 c.frame.body.recolor(u.p)
1668 u.updateMarks('old')
1669 #@+node:ekr.20201107150041.1: *4* u.undoChangeHeadline
1670 def undoChangeHeadline(self):
1671 """Undo a change to a node's headline."""
1672 c, u = self.c, self
1673 # selectPosition causes recoloring, so don't do this unless needed.
1674 if c.p != u.p: # #1333.
1675 c.selectPosition(u.p)
1676 u.p.setDirty()
1677 c.frame.body.recolor(u.p)
1678 u.p.initHeadString(u.oldHead)
1679 # This is required. Otherwise c.redraw will revert the change!
1680 c.frame.tree.setHeadline(u.p, u.oldHead)
1681 #@+node:ekr.20050424170219.1: *4* u.undoClearRecentFiles
1682 def undoClearRecentFiles(self):
1683 u = self
1684 c = u.c
1685 rf = g.app.recentFilesManager
1686 rf.setRecentFiles(u.oldRecentFiles[:])
1687 rf.createRecentFilesMenuItems(c)
1688 #@+node:ekr.20111005152227.15560: *4* u.undoCloneMarkedNodes
1689 def undoCloneMarkedNodes(self):
1690 u = self
1691 next = u.p.next()
1692 assert next.h == 'Clones of marked nodes', (u.p, next.h)
1693 next.doDelete()
1694 u.p.setAllAncestorAtFileNodesDirty()
1695 u.c.selectPosition(u.p)
1696 #@+node:ekr.20050412083057.1: *4* u.undoCloneNode
1697 def undoCloneNode(self):
1698 u = self
1699 c = u.c
1700 cc = c.chapterController
1701 if cc:
1702 cc.selectChapterByName('main')
1703 c.selectPosition(u.newP)
1704 c.deleteOutline()
1705 u.p.setDirty()
1706 c.selectPosition(u.p)
1707 #@+node:ekr.20160502175653.1: *4* u.undoCopyMarkedNodes
1708 def undoCopyMarkedNodes(self):
1709 u = self
1710 next = u.p.next()
1711 assert next.h == 'Copies of marked nodes', (u.p.h, next.h)
1712 next.doDelete()
1713 u.p.setAllAncestorAtFileNodesDirty()
1714 u.c.selectPosition(u.p)
1715 #@+node:ekr.20111005152227.15557: *4* u.undoDeleteMarkedNodes
1716 def undoDeleteMarkedNodes(self):
1717 u = self
1718 c = u.c
1719 # Undo the deletes in reverse order
1720 aList = u.deleteMarkedNodesData[:]
1721 aList.reverse()
1722 for p in aList:
1723 if p.stack:
1724 parent_v, junk = p.stack[-1]
1725 else:
1726 parent_v = c.hiddenRootNode
1727 p.v._addLink(p._childIndex, parent_v)
1728 p.v.setDirty()
1729 u.p.setAllAncestorAtFileNodesDirty()
1730 c.selectPosition(u.p)
1731 #@+node:ekr.20050412084055: *4* u.undoDeleteNode
1732 def undoDeleteNode(self):
1733 u = self
1734 c = u.c
1735 if u.oldBack:
1736 u.p._linkAfter(u.oldBack)
1737 elif u.oldParent:
1738 u.p._linkAsNthChild(u.oldParent, 0)
1739 else:
1740 u.p._linkAsRoot()
1741 u.p.setDirty()
1742 c.selectPosition(u.p)
1743 #@+node:ekr.20080425060424.10: *4* u.undoDemote
1744 def undoDemote(self):
1745 u = self
1746 c = u.c
1747 parent_v = u.p._parentVnode()
1748 n = len(u.followingSibs)
1749 # Remove the demoted nodes from p's children.
1750 u.p.v.children = u.p.v.children[: -n]
1751 # Add the demoted nodes to the parent's children.
1752 parent_v.children.extend(u.followingSibs)
1753 # Adjust the parent links.
1754 # There is no need to adjust descendant links.
1755 parent_v.setDirty()
1756 for sib in u.followingSibs:
1757 sib.parents.remove(u.p.v)
1758 sib.parents.append(parent_v)
1759 u.p.setAllAncestorAtFileNodesDirty()
1760 c.setCurrentPosition(u.p)
1761 #@+node:ekr.20050318085713: *4* u.undoGroup
1762 def undoGroup(self):
1763 """Process beads until the matching 'beforeGroup' bead is seen."""
1764 c, u = self.c, self
1765 # Remember these values.
1766 oldSel = u.oldSel
1767 p = u.p.copy() # May not exist now, but must exist later.
1768 newP = u.newP.copy() # Must exist now, but may not exist later.
1769 if g.unitTesting:
1770 assert c.positionExists(newP), repr(newP)
1771 u.groupCount += 1
1772 bunch = u.beads[u.bead]
1773 count = 0
1774 if not hasattr(bunch, 'items'):
1775 g.trace(f"oops: expecting bunch.items. got bunch.kind = {bunch.kind}")
1776 g.trace(bunch)
1777 else:
1778 # Important bug fix: 9/8/06: reverse the items first.
1779 reversedItems = bunch.items[:]
1780 reversedItems.reverse()
1781 for z in reversedItems:
1782 self.setIvarsFromBunch(z)
1783 if z.undoHelper:
1784 z.undoHelper()
1785 count += 1
1786 else:
1787 g.trace(f"oops: no undo helper for {u.undoType} {p.v}")
1788 u.groupCount -= 1
1789 u.updateMarks('old') # Bug fix: Leo 4.4.6.
1790 if not g.unitTesting and u.verboseUndoGroup:
1791 g.es("undo", count, "instances")
1792 # Helpers set dirty bits.
1793 # Set c.p, independently of helpers.
1794 if g.unitTesting:
1795 assert c.positionExists(p), repr(p)
1796 c.selectPosition(p)
1797 # Restore the selection, independently of helpers.
1798 if oldSel:
1799 i, j = oldSel
1800 c.frame.body.wrapper.setSelectionRange(i, j)
1801 #@+node:ekr.20050412083244: *4* u.undoHoistNode & undoDehoistNode
1802 def undoHoistNode(self):
1803 u = self
1804 c = u.c
1805 u.p.setDirty()
1806 c.selectPosition(u.p)
1807 c.dehoist()
1809 def undoDehoistNode(self):
1810 u = self
1811 c = u.c
1812 u.p.setDirty()
1813 c.selectPosition(u.p)
1814 c.hoist()
1815 #@+node:ekr.20050412085112: *4* u.undoInsertNode
1816 def undoInsertNode(self):
1817 u = self
1818 c = u.c
1819 cc = c.chapterController
1820 if cc:
1821 cc.selectChapterByName('main')
1822 u.newP.setAllAncestorAtFileNodesDirty()
1823 c.selectPosition(u.newP)
1824 # Bug fix: 2016/03/30.
1825 # This always selects the proper new position.
1826 # c.selectPosition(u.p)
1827 c.deleteOutline()
1828 if u.pasteAsClone:
1829 for bunch in u.beforeTree:
1830 v = bunch.v
1831 if u.p.v == v:
1832 u.p.b = bunch.body
1833 u.p.h = bunch.head
1834 else:
1835 v.setBodyString(bunch.body)
1836 v.setHeadString(bunch.head)
1837 #@+node:ekr.20050526124906: *4* u.undoMark
1838 def undoMark(self):
1839 u = self
1840 c = u.c
1841 u.updateMarks('old')
1842 if u.groupCount == 0:
1843 u.p.setDirty()
1844 c.selectPosition(u.p)
1845 #@+node:ekr.20050411112033: *4* u.undoMove
1846 def undoMove(self):
1848 u = self
1849 c = u.c
1850 cc = c.chapterController
1851 if cc:
1852 cc.selectChapterByName('main')
1853 v = u.p.v
1854 assert u.oldParent_v
1855 assert u.newParent_v
1856 assert v
1857 # Adjust the children arrays.
1858 assert u.newParent_v.children[u.newN] == v
1859 del u.newParent_v.children[u.newN]
1860 u.oldParent_v.children.insert(u.oldN, v)
1861 # Recompute the parent links.
1862 v.parents.append(u.oldParent_v)
1863 v.parents.remove(u.newParent_v)
1864 u.updateMarks('old')
1865 u.p.setDirty()
1866 c.selectPosition(u.p)
1867 #@+node:ekr.20050318085713.1: *4* u.undoNodeContents
1868 def undoNodeContents(self):
1869 """
1870 Undo all changes to the contents of a node,
1871 including headline and body text, and marked bits.
1872 """
1873 c, u = self.c, self
1874 w = c.frame.body.wrapper
1875 # selectPosition causes recoloring, so don't do this unless needed.
1876 if c.p != u.p: # #1333.
1877 c.selectPosition(u.p)
1878 u.p.setDirty()
1879 u.p.b = u.oldBody
1880 w.setAllText(u.oldBody)
1881 c.frame.body.recolor(u.p)
1882 u.p.h = u.oldHead
1883 # This is required. Otherwise c.redraw will revert the change!
1884 c.frame.tree.setHeadline(u.p, u.oldHead)
1885 if u.groupCount == 0 and u.oldSel:
1886 i, j = u.oldSel
1887 w.setSelectionRange(i, j)
1888 if u.groupCount == 0 and u.oldYScroll is not None:
1889 w.setYScrollPosition(u.oldYScroll)
1890 u.updateMarks('old')
1891 #@+node:ekr.20080425060424.14: *4* u.undoPromote
1892 def undoPromote(self):
1893 u = self
1894 c = u.c
1895 parent_v = u.p._parentVnode() # The parent of the all the *promoted* nodes.
1896 # Remove the promoted nodes from parent_v's children.
1897 n = u.p.childIndex() + 1
1898 # Adjust the old parents children
1899 old_children = parent_v.children
1900 # Add the nodes before the promoted nodes.
1901 parent_v.children = old_children[:n]
1902 # Add the nodes after the promoted nodes.
1903 parent_v.children.extend(old_children[n + len(u.children) :])
1904 # Add the demoted nodes to v's children.
1905 u.p.v.children = u.children[:]
1906 # Adjust the parent links.
1907 # There is no need to adjust descendant links.
1908 parent_v.setDirty()
1909 for child in u.children:
1910 child.parents.remove(parent_v)
1911 child.parents.append(u.p.v)
1912 u.p.setAllAncestorAtFileNodesDirty()
1913 c.setCurrentPosition(u.p)
1914 #@+node:ekr.20031218072017.1493: *4* u.undoRedoText
1915 def undoRedoText(self, p,
1916 leading, trailing, # Number of matching leading & trailing lines.
1917 oldMidLines, newMidLines, # Lists of unmatched lines.
1918 oldNewlines, newNewlines, # Number of trailing newlines.
1919 tag="undo", # "undo" or "redo"
1920 undoType=None
1921 ):
1922 """Handle text undo and redo: converts _new_ text into _old_ text."""
1923 # newNewlines is unused, but it has symmetry.
1924 u = self
1925 c = u.c
1926 w = c.frame.body.wrapper
1927 #@+<< Compute the result using p's body text >>
1928 #@+node:ekr.20061106105812.1: *5* << Compute the result using p's body text >>
1929 # Recreate the text using the present body text.
1930 body = p.b
1931 body = g.checkUnicode(body)
1932 body_lines = body.split('\n')
1933 s = []
1934 if leading > 0:
1935 s.extend(body_lines[:leading])
1936 if oldMidLines:
1937 s.extend(oldMidLines)
1938 if trailing > 0:
1939 s.extend(body_lines[-trailing :])
1940 s = '\n'.join(s)
1941 # Remove trailing newlines in s.
1942 while s and s[-1] == '\n':
1943 s = s[:-1]
1944 # Add oldNewlines newlines.
1945 if oldNewlines > 0:
1946 s = s + '\n' * oldNewlines
1947 result = s
1948 #@-<< Compute the result using p's body text >>
1949 p.setBodyString(result)
1950 p.setDirty()
1951 w.setAllText(result)
1952 sel = u.oldSel if tag == 'undo' else u.newSel
1953 if sel:
1954 i, j = sel
1955 w.setSelectionRange(i, j, insert=j)
1956 c.frame.body.recolor(p)
1957 w.seeInsertPoint() # 2009/12/21
1958 #@+node:ekr.20050408100042: *4* u.undoRedoTree
1959 def undoRedoTree(self, new_data, old_data):
1960 """Replace p and its subtree using old_data during undo."""
1961 # Same as undoReplace except uses g.Bunch.
1962 c, p, u = self.c, self.c.p, self
1963 if new_data is None:
1964 # This is the first time we have undone the operation.
1965 # Put the new data in the bead.
1966 bunch = u.beads[u.bead]
1967 bunch.newTree = u.saveTree(p.copy())
1968 u.beads[u.bead] = bunch
1969 # Replace data in tree with old data.
1970 u.restoreTree(old_data)
1971 c.setBodyString(p, p.b) # This is not a do-nothing.
1972 return p # Nothing really changes.
1973 #@+node:ekr.20080425060424.5: *4* u.undoSort
1974 def undoSort(self):
1975 u = self
1976 c = u.c
1977 parent_v = u.p._parentVnode()
1978 parent_v.children = u.oldChildren
1979 p = c.setPositionAfterSort(u.sortChildren)
1980 p.setAllAncestorAtFileNodesDirty()
1981 c.setCurrentPosition(p)
1982 #@+node:ekr.20050318085713.2: *4* u.undoTree
1983 def undoTree(self):
1984 """Redo replacement of an entire tree."""
1985 c, u = self.c, self
1986 u.p = self.undoRedoTree(u.newTree, u.oldTree)
1987 u.p.setAllAncestorAtFileNodesDirty()
1988 c.selectPosition(u.p) # Does full recolor.
1989 if u.oldSel:
1990 i, j = u.oldSel
1991 c.frame.body.wrapper.setSelectionRange(i, j)
1992 #@+node:EKR.20040526090701.4: *4* u.undoTyping
1993 def undoTyping(self):
1994 c, u = self.c, self
1995 w = c.frame.body.wrapper
1996 # selectPosition causes recoloring, so don't do this unless needed.
1997 if c.p != u.p:
1998 c.selectPosition(u.p)
1999 u.p.setDirty()
2000 u.undoRedoText(
2001 u.p, u.leading, u.trailing,
2002 u.oldMiddleLines, u.newMiddleLines,
2003 u.oldNewlines, u.newNewlines,
2004 tag="undo", undoType=u.undoType)
2005 u.updateMarks('old')
2006 if u.oldSel:
2007 c.bodyWantsFocus()
2008 i, j = u.oldSel
2009 w.setSelectionRange(i, j, insert=j)
2010 if u.yview:
2011 c.bodyWantsFocus()
2012 w.setYScrollPosition(u.yview)
2013 #@+node:ekr.20191213092304.1: *3* u.update_status
2014 def update_status(self):
2015 """
2016 Update status after either an undo or redo:
2017 """
2018 c, u = self.c, self
2019 w = c.frame.body.wrapper
2020 # Redraw and recolor.
2021 c.frame.body.updateEditors() # New in Leo 4.4.8.
2022 #
2023 # Set the new position.
2024 if 0: # Don't do this: it interferes with selection ranges.
2025 # This strange code forces a recomputation of the root position.
2026 c.selectPosition(c.p)
2027 else:
2028 c.setCurrentPosition(c.p)
2029 #
2030 # # 1451. *Always* set the changed bit.
2031 # Redrawing *must* be done here before setting u.undoing to False.
2032 i, j = w.getSelectionRange()
2033 ins = w.getInsertPoint()
2034 c.redraw()
2035 c.recolor()
2036 if u.inHead:
2037 c.editHeadline()
2038 u.inHead = False
2039 else:
2040 c.bodyWantsFocus()
2041 w.setSelectionRange(i, j, insert=ins)
2042 w.seeInsertPoint()
2043 #@-others
2044#@-others
2045#@@language python
2046#@@tabwidth -4
2047#@@pagewidth 70
2048#@-leo