Coverage for searchengine.py: 97%

158 statements  

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

1'''Define SearchEngine for search dialogs.''' 

2import re 

3 

4from tkinter import StringVar, BooleanVar, TclError 

5from tkinter import messagebox 

6 

7def get(root): 

8 '''Return the singleton SearchEngine instance for the process. 

9 

10 The single SearchEngine saves settings between dialog instances. 

11 If there is not a SearchEngine already, make one. 

12 ''' 

13 if not hasattr(root, "_searchengine"): 1l

14 root._searchengine = SearchEngine(root) 1l

15 # This creates a cycle that persists until root is deleted. 

16 return root._searchengine 1l

17 

18 

19class SearchEngine: 

20 """Handles searching a text widget for Find, Replace, and Grep.""" 

21 

22 def __init__(self, root): 

23 '''Initialize Variables that save search state. 

24 

25 The dialogs bind these to the UI elements present in the dialogs. 

26 ''' 

27 self.root = root # need for report_error() 

28 self.patvar = StringVar(root, '') # search pattern 

29 self.revar = BooleanVar(root, False) # regular expression? 

30 self.casevar = BooleanVar(root, False) # match case? 

31 self.wordvar = BooleanVar(root, False) # match whole word? 

32 self.wrapvar = BooleanVar(root, True) # wrap around buffer? 

33 self.backvar = BooleanVar(root, False) # search backwards? 

34 

35 # Access methods 

36 

37 def getpat(self): 

38 return self.patvar.get() 1gdija

39 

40 def setpat(self, pat): 

41 self.patvar.set(pat) 1gdij

42 

43 def isre(self): 

44 return self.revar.get() 1gdij

45 

46 def iscase(self): 

47 return self.casevar.get() 1di

48 

49 def isword(self): 

50 return self.wordvar.get() 1gdi

51 

52 def iswrap(self): 

53 return self.wrapvar.get() 1i

54 

55 def isback(self): 

56 return self.backvar.get() 1ia

57 

58 # Higher level access methods 

59 

60 def setcookedpat(self, pat): 

61 "Set pattern after escaping if re." 

62 # called only in search.py: 66 

63 if self.isre(): 1j

64 pat = re.escape(pat) 1j

65 self.setpat(pat) 1j

66 

67 def getcookedpat(self): 

68 pat = self.getpat() 1gd

69 if not self.isre(): # if True, see setcookedpat 1gd

70 pat = re.escape(pat) 1gd

71 if self.isword(): 1gd

72 pat = r"\b%s\b" % pat 1g

73 return pat 1gd

74 

75 def getprog(self): 

76 "Return compiled cooked search pattern." 

77 pat = self.getpat() 1da

78 if not pat: 1da

79 self.report_error(pat, "Empty regular expression") 1da

80 return None 1da

81 pat = self.getcookedpat() 1d

82 flags = 0 1d

83 if not self.iscase(): 1d

84 flags = flags | re.IGNORECASE 1d

85 try: 1d

86 prog = re.compile(pat, flags) 1d

87 except re.error as e: 1d

88 self.report_error(pat, e.msg, e.pos) 1d

89 return None 1d

90 return prog 1d

91 

92 def report_error(self, pat, msg, col=None): 

93 # Derived class could override this with something fancier 

94 msg = "Error: " + str(msg) 1dka

95 if pat: 1dka

96 msg = msg + "\nPattern: " + str(pat) 1dk

97 if col is not None: 1dka

98 msg = msg + "\nOffset: " + str(col) 1dk

99 messagebox.showerror("Regular expression error", 1dka

100 msg, master=self.root) 

101 

102 def search_text(self, text, prog=None, ok=0): 

103 '''Return (lineno, matchobj) or None for forward/backward search. 

104 

105 This function calls the right function with the right arguments. 

106 It directly return the result of that call. 

107 

108 Text is a text widget. Prog is a precompiled pattern. 

109 The ok parameter is a bit complicated as it has two effects. 

110 

111 If there is a selection, the search begin at either end, 

112 depending on the direction setting and ok, with ok meaning that 

113 the search starts with the selection. Otherwise, search begins 

114 at the insert mark. 

115 

116 To aid progress, the search functions do not return an empty 

117 match at the starting position unless ok is True. 

118 ''' 

119 

120 if not prog: 1a

121 prog = self.getprog() 1a

122 if not prog: 122 ↛ 124line 122 didn't jump to line 124, because the condition on line 122 was never false1a

123 return None # Compilation failed -- stop 1a

124 wrap = self.wrapvar.get() 1a

125 first, last = get_selection(text) 1a

126 if self.isback(): 1a

127 if ok: 1a

128 start = last 1a

129 else: 

130 start = first 1a

131 line, col = get_line_col(start) 1a

132 res = self.search_backward(text, prog, line, col, wrap, ok) 1a

133 else: 

134 if ok: 1a

135 start = first 1a

136 else: 

137 start = last 1a

138 line, col = get_line_col(start) 1a

139 res = self.search_forward(text, prog, line, col, wrap, ok) 1a

140 return res 1a

141 

142 def search_forward(self, text, prog, line, col, wrap, ok=0): 

143 wrapped = 0 1e

144 startline = line 1e

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

146 while chars: 1e

147 m = prog.search(chars[:-1], col) 1e

148 if m: 1e

149 if ok or m.end() > col: 149 ↛ 151line 149 didn't jump to line 151, because the condition on line 149 was never false1e

150 return line, m 1e

151 line = line + 1 1e

152 if wrapped and line > startline: 1e

153 break 1e

154 col = 0 1e

155 ok = 1 1e

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

157 if not chars and wrap: 1e

158 wrapped = 1 1e

159 wrap = 0 1e

160 line = 1 1e

161 chars = text.get("1.0", "2.0") 1e

162 return None 1e

163 

164 def search_backward(self, text, prog, line, col, wrap, ok=0): 

165 wrapped = 0 1b

166 startline = line 1b

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

168 while True: 1b

169 m = search_reverse(prog, chars[:-1], col) 1b

170 if m: 1b

171 if ok or m.start() < col: 171 ↛ 173line 171 didn't jump to line 173, because the condition on line 171 was never false1b

172 return line, m 1b

173 line = line - 1 1b

174 if wrapped and line < startline: 1b

175 break 1b

176 ok = 1 1b

177 if line <= 0: 1b

178 if not wrap: 1b

179 break 1b

180 wrapped = 1 1b

181 wrap = 0 1b

182 pos = text.index("end-1c") 1b

183 line, col = map(int, pos.split(".")) 1b

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

185 col = len(chars) - 1 1b

186 return None 1b

187 

188 

189def search_reverse(prog, chars, col): 

190 '''Search backwards and return an re match object or None. 

191 

192 This is done by searching forwards until there is no match. 

193 Prog: compiled re object with a search method returning a match. 

194 Chars: line of text, without \\n. 

195 Col: stop index for the search; the limit for match.end(). 

196 ''' 

197 m = prog.search(chars) 1bf

198 if not m: 1bf

199 return None 1b

200 found = None 1bf

201 i, j = m.span() # m.start(), m.end() == match slice indexes 1bf

202 while i < col and j <= col: 1bf

203 found = m 1bf

204 if i == j: 1bf

205 j = j+1 1b

206 m = prog.search(chars, j) 1bf

207 if not m: 1bf

208 break 1bf

209 i, j = m.span() 1bf

210 return found 1bf

211 

212def get_selection(text): 

213 '''Return tuple of 'line.col' indexes from selection or insert mark. 

214 ''' 

215 try: 1ha

216 first = text.index("sel.first") 1ha

217 last = text.index("sel.last") 1ha

218 except TclError: 1ha

219 first = last = None 1ha

220 if not first: 1ha

221 first = text.index("insert") 1ha

222 if not last: 1ha

223 last = first 1ha

224 return first, last 1ha

225 

226def get_line_col(index): 

227 '''Return (line, col) tuple of ints from 'line.col' string.''' 

228 line, col = map(int, index.split(".")) # Fails on invalid index 1ma

229 return line, col 1ma

230 

231 

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

233 from unittest import main 

234 main('idlelib.idle_test.test_searchengine', verbosity=2)