| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  | '''Define SearchEngine for search dialogs.''' | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | import re | 
					
						
							| 
									
										
										
										
											2016-08-31 00:50:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  | from tkinter import StringVar, BooleanVar, TclError | 
					
						
							| 
									
										
										
										
											2021-01-25 06:33:18 -05:00
										 |  |  | from tkinter import messagebox | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | def get(root): | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |     '''Return the singleton SearchEngine instance for the process.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     The single SearchEngine saves settings between dialog instances. | 
					
						
							|  |  |  |     If there is not a SearchEngine already, make one. | 
					
						
							|  |  |  |     '''
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |     if not hasattr(root, "_searchengine"): | 
					
						
							|  |  |  |         root._searchengine = SearchEngine(root) | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |         # This creates a cycle that persists until root is deleted. | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |     return root._searchengine | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-31 00:50:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | class SearchEngine: | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |     """Handles searching a text widget for Find, Replace, and Grep.""" | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def __init__(self, root): | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |         '''Initialize Variables that save search state.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         The dialogs bind these to the UI elements present in the dialogs. | 
					
						
							|  |  |  |         '''
 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |         self.root = root  # need for report_error() | 
					
						
							|  |  |  |         self.patvar = StringVar(root, '')   # search pattern | 
					
						
							|  |  |  |         self.revar = BooleanVar(root, False)   # regular expression? | 
					
						
							|  |  |  |         self.casevar = BooleanVar(root, False)   # match case? | 
					
						
							|  |  |  |         self.wordvar = BooleanVar(root, False)   # match whole word? | 
					
						
							|  |  |  |         self.wrapvar = BooleanVar(root, True)   # wrap around buffer? | 
					
						
							|  |  |  |         self.backvar = BooleanVar(root, False)   # search backwards? | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |     # Access methods | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def getpat(self): | 
					
						
							|  |  |  |         return self.patvar.get() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def setpat(self, pat): | 
					
						
							|  |  |  |         self.patvar.set(pat) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def isre(self): | 
					
						
							|  |  |  |         return self.revar.get() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def iscase(self): | 
					
						
							|  |  |  |         return self.casevar.get() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def isword(self): | 
					
						
							|  |  |  |         return self.wordvar.get() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def iswrap(self): | 
					
						
							|  |  |  |         return self.wrapvar.get() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def isback(self): | 
					
						
							|  |  |  |         return self.backvar.get() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     # Higher level access methods | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |     def setcookedpat(self, pat): | 
					
						
							|  |  |  |         "Set pattern after escaping if re." | 
					
						
							| 
									
										
										
										
											2016-05-28 13:22:31 -04:00
										 |  |  |         # called only in search.py: 66 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |         if self.isre(): | 
					
						
							|  |  |  |             pat = re.escape(pat) | 
					
						
							|  |  |  |         self.setpat(pat) | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |     def getcookedpat(self): | 
					
						
							|  |  |  |         pat = self.getpat() | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |         if not self.isre():  # if True, see setcookedpat | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |             pat = re.escape(pat) | 
					
						
							|  |  |  |         if self.isword(): | 
					
						
							|  |  |  |             pat = r"\b%s\b" % pat | 
					
						
							|  |  |  |         return pat | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def getprog(self): | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |         "Return compiled cooked search pattern." | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |         pat = self.getpat() | 
					
						
							|  |  |  |         if not pat: | 
					
						
							|  |  |  |             self.report_error(pat, "Empty regular expression") | 
					
						
							|  |  |  |             return None | 
					
						
							|  |  |  |         pat = self.getcookedpat() | 
					
						
							|  |  |  |         flags = 0 | 
					
						
							|  |  |  |         if not self.iscase(): | 
					
						
							|  |  |  |             flags = flags | re.IGNORECASE | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             prog = re.compile(pat, flags) | 
					
						
							| 
									
										
										
										
											2023-12-11 15:45:08 -05:00
										 |  |  |         except re.PatternError as e: | 
					
						
							| 
									
										
										
										
											2020-11-22 07:06:51 +02:00
										 |  |  |             self.report_error(pat, e.msg, e.pos) | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |             return None | 
					
						
							|  |  |  |         return prog | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-22 07:06:51 +02:00
										 |  |  |     def report_error(self, pat, msg, col=None): | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |         # Derived class could override this with something fancier | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |         msg = "Error: " + str(msg) | 
					
						
							|  |  |  |         if pat: | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |             msg = msg + "\nPattern: " + str(pat) | 
					
						
							| 
									
										
										
										
											2020-11-22 07:06:51 +02:00
										 |  |  |         if col is not None: | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |             msg = msg + "\nOffset: " + str(col) | 
					
						
							| 
									
										
										
										
											2021-01-25 06:33:18 -05:00
										 |  |  |         messagebox.showerror("Regular expression error", | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |                                msg, master=self.root) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def search_text(self, text, prog=None, ok=0): | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |         '''Return (lineno, matchobj) or None for forward/backward search.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |         This function calls the right function with the right arguments. | 
					
						
							|  |  |  |         It directly return the result of that call. | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |         Text is a text widget. Prog is a precompiled pattern. | 
					
						
							| 
									
										
										
										
											2015-04-14 09:30:01 +03:00
										 |  |  |         The ok parameter is a bit complicated as it has two effects. | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |         If there is a selection, the search begin at either end, | 
					
						
							|  |  |  |         depending on the direction setting and ok, with ok meaning that | 
					
						
							|  |  |  |         the search starts with the selection. Otherwise, search begins | 
					
						
							|  |  |  |         at the insert mark. | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |         To aid progress, the search functions do not return an empty | 
					
						
							|  |  |  |         match at the starting position unless ok is True. | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |         '''
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  |         if not prog: | 
					
						
							|  |  |  |             prog = self.getprog() | 
					
						
							|  |  |  |             if not prog: | 
					
						
							|  |  |  |                 return None # Compilation failed -- stop | 
					
						
							|  |  |  |         wrap = self.wrapvar.get() | 
					
						
							|  |  |  |         first, last = get_selection(text) | 
					
						
							|  |  |  |         if self.isback(): | 
					
						
							|  |  |  |             if ok: | 
					
						
							|  |  |  |                 start = last | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 start = first | 
					
						
							|  |  |  |             line, col = get_line_col(start) | 
					
						
							|  |  |  |             res = self.search_backward(text, prog, line, col, wrap, ok) | 
					
						
							|  |  |  |         else: | 
					
						
							|  |  |  |             if ok: | 
					
						
							|  |  |  |                 start = first | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 start = last | 
					
						
							|  |  |  |             line, col = get_line_col(start) | 
					
						
							|  |  |  |             res = self.search_forward(text, prog, line, col, wrap, ok) | 
					
						
							|  |  |  |         return res | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def search_forward(self, text, prog, line, col, wrap, ok=0): | 
					
						
							|  |  |  |         wrapped = 0 | 
					
						
							|  |  |  |         startline = line | 
					
						
							|  |  |  |         chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
					
						
							|  |  |  |         while chars: | 
					
						
							|  |  |  |             m = prog.search(chars[:-1], col) | 
					
						
							|  |  |  |             if m: | 
					
						
							|  |  |  |                 if ok or m.end() > col: | 
					
						
							|  |  |  |                     return line, m | 
					
						
							|  |  |  |             line = line + 1 | 
					
						
							|  |  |  |             if wrapped and line > startline: | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  |             col = 0 | 
					
						
							|  |  |  |             ok = 1 | 
					
						
							|  |  |  |             chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
					
						
							|  |  |  |             if not chars and wrap: | 
					
						
							|  |  |  |                 wrapped = 1 | 
					
						
							|  |  |  |                 wrap = 0 | 
					
						
							|  |  |  |                 line = 1 | 
					
						
							|  |  |  |                 chars = text.get("1.0", "2.0") | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def search_backward(self, text, prog, line, col, wrap, ok=0): | 
					
						
							|  |  |  |         wrapped = 0 | 
					
						
							|  |  |  |         startline = line | 
					
						
							|  |  |  |         chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
					
						
							| 
									
										
										
										
											2022-07-13 21:09:07 -04:00
										 |  |  |         while True: | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |             m = search_reverse(prog, chars[:-1], col) | 
					
						
							|  |  |  |             if m: | 
					
						
							|  |  |  |                 if ok or m.start() < col: | 
					
						
							|  |  |  |                     return line, m | 
					
						
							|  |  |  |             line = line - 1 | 
					
						
							|  |  |  |             if wrapped and line < startline: | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  |             ok = 1 | 
					
						
							|  |  |  |             if line <= 0: | 
					
						
							|  |  |  |                 if not wrap: | 
					
						
							|  |  |  |                     break | 
					
						
							|  |  |  |                 wrapped = 1 | 
					
						
							|  |  |  |                 wrap = 0 | 
					
						
							|  |  |  |                 pos = text.index("end-1c") | 
					
						
							| 
									
										
										
										
											2002-09-18 03:14:11 +00:00
										 |  |  |                 line, col = map(int, pos.split(".")) | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |             chars = text.get("%d.0" % line, "%d.0" % (line+1)) | 
					
						
							|  |  |  |             col = len(chars) - 1 | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2016-08-31 00:50:55 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  | def search_reverse(prog, chars, col): | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |     '''Search backwards and return an re match object or None.
 | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     This is done by searching forwards until there is no match. | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |     Prog: compiled re object with a search method returning a match. | 
					
						
							| 
									
										
										
										
											2015-04-03 18:12:41 +03:00
										 |  |  |     Chars: line of text, without \\n. | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |     Col: stop index for the search; the limit for match.end(). | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |     '''
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |     m = prog.search(chars) | 
					
						
							|  |  |  |     if not m: | 
					
						
							|  |  |  |         return None | 
					
						
							|  |  |  |     found = None | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  |     i, j = m.span()  # m.start(), m.end() == match slice indexes | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |     while i < col and j <= col: | 
					
						
							|  |  |  |         found = m | 
					
						
							|  |  |  |         if i == j: | 
					
						
							|  |  |  |             j = j+1 | 
					
						
							|  |  |  |         m = prog.search(chars, j) | 
					
						
							|  |  |  |         if not m: | 
					
						
							|  |  |  |             break | 
					
						
							|  |  |  |         i, j = m.span() | 
					
						
							|  |  |  |     return found | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_selection(text): | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |     '''Return tuple of 'line.col' indexes from selection or insert mark.
 | 
					
						
							|  |  |  |     '''
 | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |     try: | 
					
						
							|  |  |  |         first = text.index("sel.first") | 
					
						
							|  |  |  |         last = text.index("sel.last") | 
					
						
							|  |  |  |     except TclError: | 
					
						
							|  |  |  |         first = last = None | 
					
						
							|  |  |  |     if not first: | 
					
						
							|  |  |  |         first = text.index("insert") | 
					
						
							|  |  |  |     if not last: | 
					
						
							|  |  |  |         last = first | 
					
						
							|  |  |  |     return first, last | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | def get_line_col(index): | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  |     '''Return (line, col) tuple of ints from 'line.col' string.''' | 
					
						
							| 
									
										
										
										
											2002-09-18 03:14:11 +00:00
										 |  |  |     line, col = map(int, index.split(".")) # Fails on invalid index | 
					
						
							| 
									
										
										
										
											2000-08-15 01:13:23 +00:00
										 |  |  |     return line, col | 
					
						
							| 
									
										
										
										
											2013-08-19 01:05:19 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-06-19 19:12:52 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2013-08-31 16:27:16 -04:00
										 |  |  | if __name__ == "__main__": | 
					
						
							| 
									
										
										
										
											2018-06-19 19:12:52 -04:00
										 |  |  |     from unittest import main | 
					
						
							|  |  |  |     main('idlelib.idle_test.test_searchengine', verbosity=2) |