Coverage for replace.py: 10%

193 statements  

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

1"""Replace dialog for IDLE. Inherits SearchDialogBase for GUI. 

2Uses idlelib.searchengine.SearchEngine for search capability. 

3Defines various replace related functions like replace, replace all, 

4and replace+find. 

5""" 

6import re 

7 

8from tkinter import StringVar, TclError 

9 

10from idlelib.searchbase import SearchDialogBase 

11from idlelib import searchengine 

12 

13 

14def replace(text, insert_tags=None): 

15 """Create or reuse a singleton ReplaceDialog instance. 

16 

17 The singleton dialog saves user entries and preferences 

18 across instances. 

19 

20 Args: 

21 text: Text widget containing the text to be searched. 

22 """ 

23 root = text._root() 

24 engine = searchengine.get(root) 

25 if not hasattr(engine, "_replacedialog"): 

26 engine._replacedialog = ReplaceDialog(root, engine) 

27 dialog = engine._replacedialog 

28 dialog.open(text, insert_tags=insert_tags) 

29 

30 

31class ReplaceDialog(SearchDialogBase): 

32 "Dialog for finding and replacing a pattern in text." 

33 

34 title = "Replace Dialog" 

35 icon = "Replace" 

36 

37 def __init__(self, root, engine): 

38 """Create search dialog for finding and replacing text. 

39 

40 Uses SearchDialogBase as the basis for the GUI and a 

41 searchengine instance to prepare the search. 

42 

43 Attributes: 

44 replvar: StringVar containing 'Replace with:' value. 

45 replent: Entry widget for replvar. Created in 

46 create_entries(). 

47 ok: Boolean used in searchengine.search_text to indicate 

48 whether the search includes the selection. 

49 """ 

50 super().__init__(root, engine) 

51 self.replvar = StringVar(root) 

52 self.insert_tags = None 

53 

54 def open(self, text, insert_tags=None): 

55 """Make dialog visible on top of others and ready to use. 

56 

57 Also, highlight the currently selected text and set the 

58 search to include the current selection (self.ok). 

59 

60 Args: 

61 text: Text widget being searched. 

62 """ 

63 SearchDialogBase.open(self, text) 

64 try: 

65 first = text.index("sel.first") 

66 except TclError: 

67 first = None 

68 try: 

69 last = text.index("sel.last") 

70 except TclError: 

71 last = None 

72 first = first or text.index("insert") 

73 last = last or first 

74 self.show_hit(first, last) 

75 self.ok = True 

76 self.insert_tags = insert_tags 

77 

78 def create_entries(self): 

79 "Create base and additional label and text entry widgets." 

80 SearchDialogBase.create_entries(self) 

81 self.replent = self.make_entry("Replace with:", self.replvar)[0] 

82 

83 def create_command_buttons(self): 

84 """Create base and additional command buttons. 

85 

86 The additional buttons are for Find, Replace, 

87 Replace+Find, and Replace All. 

88 """ 

89 SearchDialogBase.create_command_buttons(self) 

90 self.make_button("Find", self.find_it) 

91 self.make_button("Replace", self.replace_it) 

92 self.make_button("Replace+Find", self.default_command, isdef=True) 

93 self.make_button("Replace All", self.replace_all) 

94 

95 def find_it(self, event=None): 

96 "Handle the Find button." 

97 self.do_find(False) 

98 

99 def replace_it(self, event=None): 

100 """Handle the Replace button. 

101 

102 If the find is successful, then perform replace. 

103 """ 

104 if self.do_find(self.ok): 

105 self.do_replace() 

106 

107 def default_command(self, event=None): 

108 """Handle the Replace+Find button as the default command. 

109 

110 First performs a replace and then, if the replace was 

111 successful, a find next. 

112 """ 

113 if self.do_find(self.ok): 

114 if self.do_replace(): # Only find next match if replace succeeded. 

115 # A bad re can cause it to fail. 

116 self.do_find(False) 

117 

118 def _replace_expand(self, m, repl): 

119 "Expand replacement text if regular expression." 

120 if self.engine.isre(): 

121 try: 

122 new = m.expand(repl) 

123 except re.error: 

124 self.engine.report_error(repl, 'Invalid Replace Expression') 

125 new = None 

126 else: 

127 new = repl 

128 

129 return new 

130 

131 def replace_all(self, event=None): 

132 """Handle the Replace All button. 

133 

134 Search text for occurrences of the Find value and replace 

135 each of them. The 'wrap around' value controls the start 

136 point for searching. If wrap isn't set, then the searching 

137 starts at the first occurrence after the current selection; 

138 if wrap is set, the replacement starts at the first line. 

139 The replacement is always done top-to-bottom in the text. 

140 """ 

141 prog = self.engine.getprog() 

142 if not prog: 

143 return 

144 repl = self.replvar.get() 

145 text = self.text 

146 res = self.engine.search_text(text, prog) 

147 if not res: 

148 self.bell() 

149 return 

150 text.tag_remove("sel", "1.0", "end") 

151 text.tag_remove("hit", "1.0", "end") 

152 line = res[0] 

153 col = res[1].start() 

154 if self.engine.iswrap(): 

155 line = 1 

156 col = 0 

157 ok = True 

158 first = last = None 

159 # XXX ought to replace circular instead of top-to-bottom when wrapping 

160 text.undo_block_start() 

161 while res := self.engine.search_forward( 

162 text, prog, line, col, wrap=False, ok=ok): 

163 line, m = res 

164 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 

165 orig = m.group() 

166 new = self._replace_expand(m, repl) 

167 if new is None: 

168 break 

169 i, j = m.span() 

170 first = "%d.%d" % (line, i) 

171 last = "%d.%d" % (line, j) 

172 if new == orig: 

173 text.mark_set("insert", last) 

174 else: 

175 text.mark_set("insert", first) 

176 if first != last: 

177 text.delete(first, last) 

178 if new: 

179 text.insert(first, new, self.insert_tags) 

180 col = i + len(new) 

181 ok = False 

182 text.undo_block_stop() 

183 if first and last: 

184 self.show_hit(first, last) 

185 self.close() 

186 

187 def do_find(self, ok=False): 

188 """Search for and highlight next occurrence of pattern in text. 

189 

190 No text replacement is done with this option. 

191 """ 

192 if not self.engine.getprog(): 

193 return False 

194 text = self.text 

195 res = self.engine.search_text(text, None, ok) 

196 if not res: 

197 self.bell() 

198 return False 

199 line, m = res 

200 i, j = m.span() 

201 first = "%d.%d" % (line, i) 

202 last = "%d.%d" % (line, j) 

203 self.show_hit(first, last) 

204 self.ok = True 

205 return True 

206 

207 def do_replace(self): 

208 "Replace search pattern in text with replacement value." 

209 prog = self.engine.getprog() 

210 if not prog: 

211 return False 

212 text = self.text 

213 try: 

214 first = pos = text.index("sel.first") 

215 last = text.index("sel.last") 

216 except TclError: 

217 pos = None 

218 if not pos: 

219 first = last = pos = text.index("insert") 

220 line, col = searchengine.get_line_col(pos) 

221 chars = text.get("%d.0" % line, "%d.0" % (line+1)) 

222 m = prog.match(chars, col) 

223 if not prog: 

224 return False 

225 new = self._replace_expand(m, self.replvar.get()) 

226 if new is None: 

227 return False 

228 text.mark_set("insert", first) 

229 text.undo_block_start() 

230 if m.group(): 

231 text.delete(first, last) 

232 if new: 

233 text.insert(first, new, self.insert_tags) 

234 text.undo_block_stop() 

235 self.show_hit(first, text.index("insert")) 

236 self.ok = False 

237 return True 

238 

239 def show_hit(self, first, last): 

240 """Highlight text between first and last indices. 

241 

242 Text is highlighted via the 'hit' tag and the marked 

243 section is brought into view. 

244 

245 The colors from the 'hit' tag aren't currently shown 

246 when the text is displayed. This is due to the 'sel' 

247 tag being added first, so the colors in the 'sel' 

248 config are seen instead of the colors for 'hit'. 

249 """ 

250 text = self.text 

251 text.mark_set("insert", first) 

252 text.tag_remove("sel", "1.0", "end") 

253 text.tag_add("sel", first, last) 

254 text.tag_remove("hit", "1.0", "end") 

255 if first == last: 

256 text.tag_add("hit", first) 

257 else: 

258 text.tag_add("hit", first, last) 

259 text.see("insert") 

260 text.update_idletasks() 

261 

262 def close(self, event=None): 

263 "Close the dialog and remove hit tags." 

264 SearchDialogBase.close(self, event) 

265 self.text.tag_remove("hit", "1.0", "end") 

266 self.insert_tags = None 

267 

268 

269def _replace_dialog(parent): # htest # 

270 from tkinter import Toplevel, Text, END, SEL 

271 from tkinter.ttk import Frame, Button 

272 

273 top = Toplevel(parent) 

274 top.title("Test ReplaceDialog") 

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

276 top.geometry("+%d+%d" % (x, y + 175)) 

277 

278 # mock undo delegator methods 

279 def undo_block_start(): 

280 pass 

281 

282 def undo_block_stop(): 

283 pass 

284 

285 frame = Frame(top) 

286 frame.pack() 

287 text = Text(frame, inactiveselectbackground='gray') 

288 text.undo_block_start = undo_block_start 

289 text.undo_block_stop = undo_block_stop 

290 text.pack() 

291 text.insert("insert","This is a sample sTring\nPlus MORE.") 

292 text.focus_set() 

293 

294 def show_replace(): 

295 text.tag_add(SEL, "1.0", END) 

296 replace(text) 

297 text.tag_remove(SEL, "1.0", END) 

298 

299 button = Button(frame, text="Replace", command=show_replace) 

300 button.pack() 

301 

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

303 from unittest import main 

304 main('idlelib.idle_test.test_replace', verbosity=2, exit=False) 

305 

306 from idlelib.idle_test.htest import run 

307 run(_replace_dialog)