Coverage for undo.py: 17%

257 statements  

« prev     ^ index     » next       coverage.py v7.2.5, created at 2023-05-11 13:22 -0700

1import string 

2 

3from idlelib.delegator import Delegator 

4 

5# tkinter import not needed because module does not create widgets, 

6# although many methods operate on text widget arguments. 

7 

8#$ event <<redo>> 

9#$ win <Control-y> 

10#$ unix <Alt-z> 

11 

12#$ event <<undo>> 

13#$ win <Control-z> 

14#$ unix <Control-z> 

15 

16#$ event <<dump-undo-state>> 

17#$ win <Control-backslash> 

18#$ unix <Control-backslash> 

19 

20 

21class UndoDelegator(Delegator): 

22 

23 max_undo = 1000 

24 

25 def __init__(self): 

26 Delegator.__init__(self) 

27 self.reset_undo() 

28 

29 def setdelegate(self, delegate): 

30 if self.delegate is not None: 

31 self.unbind("<<undo>>") 

32 self.unbind("<<redo>>") 

33 self.unbind("<<dump-undo-state>>") 

34 Delegator.setdelegate(self, delegate) 

35 if delegate is not None: 

36 self.bind("<<undo>>", self.undo_event) 

37 self.bind("<<redo>>", self.redo_event) 

38 self.bind("<<dump-undo-state>>", self.dump_event) 

39 

40 def dump_event(self, event): 

41 from pprint import pprint 

42 pprint(self.undolist[:self.pointer]) 

43 print("pointer:", self.pointer, end=' ') 

44 print("saved:", self.saved, end=' ') 

45 print("can_merge:", self.can_merge, end=' ') 

46 print("get_saved():", self.get_saved()) 

47 pprint(self.undolist[self.pointer:]) 

48 return "break" 

49 

50 def reset_undo(self): 

51 self.was_saved = -1 

52 self.pointer = 0 

53 self.undolist = [] 

54 self.undoblock = 0 # or a CommandSequence instance 

55 self.set_saved(1) 

56 

57 def set_saved(self, flag): 

58 if flag: 

59 self.saved = self.pointer 

60 else: 

61 self.saved = -1 

62 self.can_merge = False 

63 self.check_saved() 

64 

65 def get_saved(self): 

66 return self.saved == self.pointer 

67 

68 saved_change_hook = None 

69 

70 def set_saved_change_hook(self, hook): 

71 self.saved_change_hook = hook 

72 

73 was_saved = -1 

74 

75 def check_saved(self): 

76 is_saved = self.get_saved() 

77 if is_saved != self.was_saved: 

78 self.was_saved = is_saved 

79 if self.saved_change_hook: 

80 self.saved_change_hook() 

81 

82 def insert(self, index, chars, tags=None): 

83 self.addcmd(InsertCommand(index, chars, tags)) 

84 

85 def delete(self, index1, index2=None): 

86 self.addcmd(DeleteCommand(index1, index2)) 

87 

88 # Clients should call undo_block_start() and undo_block_stop() 

89 # around a sequence of editing cmds to be treated as a unit by 

90 # undo & redo. Nested matching calls are OK, and the inner calls 

91 # then act like nops. OK too if no editing cmds, or only one 

92 # editing cmd, is issued in between: if no cmds, the whole 

93 # sequence has no effect; and if only one cmd, that cmd is entered 

94 # directly into the undo list, as if undo_block_xxx hadn't been 

95 # called. The intent of all that is to make this scheme easy 

96 # to use: all the client has to worry about is making sure each 

97 # _start() call is matched by a _stop() call. 

98 

99 def undo_block_start(self): 

100 if self.undoblock == 0: 

101 self.undoblock = CommandSequence() 

102 self.undoblock.bump_depth() 

103 

104 def undo_block_stop(self): 

105 if self.undoblock.bump_depth(-1) == 0: 

106 cmd = self.undoblock 

107 self.undoblock = 0 

108 if len(cmd) > 0: 

109 if len(cmd) == 1: 

110 # no need to wrap a single cmd 

111 cmd = cmd.getcmd(0) 

112 # this blk of cmds, or single cmd, has already 

113 # been done, so don't execute it again 

114 self.addcmd(cmd, 0) 

115 

116 def addcmd(self, cmd, execute=True): 

117 if execute: 

118 cmd.do(self.delegate) 

119 if self.undoblock != 0: 

120 self.undoblock.append(cmd) 

121 return 

122 if self.can_merge and self.pointer > 0: 

123 lastcmd = self.undolist[self.pointer-1] 

124 if lastcmd.merge(cmd): 

125 return 

126 self.undolist[self.pointer:] = [cmd] 

127 if self.saved > self.pointer: 

128 self.saved = -1 

129 self.pointer = self.pointer + 1 

130 if len(self.undolist) > self.max_undo: 

131 ##print "truncating undo list" 

132 del self.undolist[0] 

133 self.pointer = self.pointer - 1 

134 if self.saved >= 0: 

135 self.saved = self.saved - 1 

136 self.can_merge = True 

137 self.check_saved() 

138 

139 def undo_event(self, event): 

140 if self.pointer == 0: 

141 self.bell() 

142 return "break" 

143 cmd = self.undolist[self.pointer - 1] 

144 cmd.undo(self.delegate) 

145 self.pointer = self.pointer - 1 

146 self.can_merge = False 

147 self.check_saved() 

148 return "break" 

149 

150 def redo_event(self, event): 

151 if self.pointer >= len(self.undolist): 

152 self.bell() 

153 return "break" 

154 cmd = self.undolist[self.pointer] 

155 cmd.redo(self.delegate) 

156 self.pointer = self.pointer + 1 

157 self.can_merge = False 

158 self.check_saved() 

159 return "break" 

160 

161 

162class Command: 

163 # Base class for Undoable commands 

164 

165 tags = None 

166 

167 def __init__(self, index1, index2, chars, tags=None): 

168 self.marks_before = {} 

169 self.marks_after = {} 

170 self.index1 = index1 

171 self.index2 = index2 

172 self.chars = chars 

173 if tags: 

174 self.tags = tags 

175 

176 def __repr__(self): 

177 s = self.__class__.__name__ 

178 t = (self.index1, self.index2, self.chars, self.tags) 

179 if self.tags is None: 

180 t = t[:-1] 

181 return s + repr(t) 

182 

183 def do(self, text): 

184 pass 

185 

186 def redo(self, text): 

187 pass 

188 

189 def undo(self, text): 

190 pass 

191 

192 def merge(self, cmd): 

193 return 0 

194 

195 def save_marks(self, text): 

196 marks = {} 

197 for name in text.mark_names(): 

198 if name != "insert" and name != "current": 

199 marks[name] = text.index(name) 

200 return marks 

201 

202 def set_marks(self, text, marks): 

203 for name, index in marks.items(): 

204 text.mark_set(name, index) 

205 

206 

207class InsertCommand(Command): 

208 # Undoable insert command 

209 

210 def __init__(self, index1, chars, tags=None): 

211 Command.__init__(self, index1, None, chars, tags) 

212 

213 def do(self, text): 

214 self.marks_before = self.save_marks(text) 

215 self.index1 = text.index(self.index1) 

216 if text.compare(self.index1, ">", "end-1c"): 

217 # Insert before the final newline 

218 self.index1 = text.index("end-1c") 

219 text.insert(self.index1, self.chars, self.tags) 

220 self.index2 = text.index("%s+%dc" % (self.index1, len(self.chars))) 

221 self.marks_after = self.save_marks(text) 

222 ##sys.__stderr__.write("do: %s\n" % self) 

223 

224 def redo(self, text): 

225 text.mark_set('insert', self.index1) 

226 text.insert(self.index1, self.chars, self.tags) 

227 self.set_marks(text, self.marks_after) 

228 text.see('insert') 

229 ##sys.__stderr__.write("redo: %s\n" % self) 

230 

231 def undo(self, text): 

232 text.mark_set('insert', self.index1) 

233 text.delete(self.index1, self.index2) 

234 self.set_marks(text, self.marks_before) 

235 text.see('insert') 

236 ##sys.__stderr__.write("undo: %s\n" % self) 

237 

238 def merge(self, cmd): 

239 if self.__class__ is not cmd.__class__: 

240 return False 

241 if self.index2 != cmd.index1: 

242 return False 

243 if self.tags != cmd.tags: 

244 return False 

245 if len(cmd.chars) != 1: 

246 return False 

247 if self.chars and \ 

248 self.classify(self.chars[-1]) != self.classify(cmd.chars): 

249 return False 

250 self.index2 = cmd.index2 

251 self.chars = self.chars + cmd.chars 

252 return True 

253 

254 alphanumeric = string.ascii_letters + string.digits + "_" 

255 

256 def classify(self, c): 

257 if c in self.alphanumeric: 

258 return "alphanumeric" 

259 if c == "\n": 

260 return "newline" 

261 return "punctuation" 

262 

263 

264class DeleteCommand(Command): 

265 # Undoable delete command 

266 

267 def __init__(self, index1, index2=None): 

268 Command.__init__(self, index1, index2, None, None) 

269 

270 def do(self, text): 

271 self.marks_before = self.save_marks(text) 

272 self.index1 = text.index(self.index1) 

273 if self.index2: 

274 self.index2 = text.index(self.index2) 

275 else: 

276 self.index2 = text.index(self.index1 + " +1c") 

277 if text.compare(self.index2, ">", "end-1c"): 

278 # Don't delete the final newline 

279 self.index2 = text.index("end-1c") 

280 self.chars = text.get(self.index1, self.index2) 

281 text.delete(self.index1, self.index2) 

282 self.marks_after = self.save_marks(text) 

283 ##sys.__stderr__.write("do: %s\n" % self) 

284 

285 def redo(self, text): 

286 text.mark_set('insert', self.index1) 

287 text.delete(self.index1, self.index2) 

288 self.set_marks(text, self.marks_after) 

289 text.see('insert') 

290 ##sys.__stderr__.write("redo: %s\n" % self) 

291 

292 def undo(self, text): 

293 text.mark_set('insert', self.index1) 

294 text.insert(self.index1, self.chars) 

295 self.set_marks(text, self.marks_before) 

296 text.see('insert') 

297 ##sys.__stderr__.write("undo: %s\n" % self) 

298 

299 

300class CommandSequence(Command): 

301 # Wrapper for a sequence of undoable cmds to be undone/redone 

302 # as a unit 

303 

304 def __init__(self): 

305 self.cmds = [] 

306 self.depth = 0 

307 

308 def __repr__(self): 

309 s = self.__class__.__name__ 

310 strs = [] 

311 for cmd in self.cmds: 

312 strs.append(" %r" % (cmd,)) 

313 return s + "(\n" + ",\n".join(strs) + "\n)" 

314 

315 def __len__(self): 

316 return len(self.cmds) 

317 

318 def append(self, cmd): 

319 self.cmds.append(cmd) 

320 

321 def getcmd(self, i): 

322 return self.cmds[i] 

323 

324 def redo(self, text): 

325 for cmd in self.cmds: 

326 cmd.redo(text) 

327 

328 def undo(self, text): 

329 cmds = self.cmds[:] 

330 cmds.reverse() 

331 for cmd in cmds: 

332 cmd.undo(text) 

333 

334 def bump_depth(self, incr=1): 

335 self.depth = self.depth + incr 

336 return self.depth 

337 

338 

339def _undo_delegator(parent): # htest # 

340 from tkinter import Toplevel, Text, Button 

341 from idlelib.percolator import Percolator 

342 undowin = Toplevel(parent) 

343 undowin.title("Test UndoDelegator") 

344 x, y = map(int, parent.geometry().split('+')[1:]) 

345 undowin.geometry("+%d+%d" % (x, y + 175)) 

346 

347 text = Text(undowin, height=10) 

348 text.pack() 

349 text.focus_set() 

350 p = Percolator(text) 

351 d = UndoDelegator() 

352 p.insertfilter(d) 

353 

354 undo = Button(undowin, text="Undo", command=lambda:d.undo_event(None)) 

355 undo.pack(side='left') 

356 redo = Button(undowin, text="Redo", command=lambda:d.redo_event(None)) 

357 redo.pack(side='left') 

358 dump = Button(undowin, text="Dump", command=lambda:d.dump_event(None)) 

359 dump.pack(side='left') 

360 

361if __name__ == "__main__": 361 ↛ 362line 361 didn't jump to line 362, because the condition on line 361 was never true

362 from unittest import main 

363 main('idlelib.idle_test.test_undo', verbosity=2, exit=False) 

364 

365 from idlelib.idle_test.htest import run 

366 run(_undo_delegator)