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

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) 

165 

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): 

185 

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 

211 

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. 

236 

237 def setRedoType(self, theType): 

238 

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): 

261 

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): 

285 

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. 

406 

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) 

516 

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 

763 

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. 

788 

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 

795 

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. 

893 

894 def canRedo(self): 

895 u = self 

896 return u.redoMenuLabel != "Can't Redo" 

897 

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. 

904 

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. 

919 

920 Do nothing when called from the undo/redo logic because the Undo 

921 and Redo commands merely reset the bead pointer. 

922 

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. 

926 

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) 

1153 

1154 # Compatibility 

1155 

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. 

1167 

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. 

1171 

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. 

1174 

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): 

1193 

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): 

1202 

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. 

1224 

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() 

1453 

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() 

1808 

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): 

1847 

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