Home | History | Annotate | Download | only in idlelib
      1 '''Define SearchEngine for search dialogs.'''
      2 import re
      3 
      4 from tkinter import StringVar, BooleanVar, TclError
      5 import tkinter.messagebox as tkMessageBox
      6 
      7 def 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"):
     14         root._searchengine = SearchEngine(root)
     15         # This creates a cycle that persists until root is deleted.
     16     return root._searchengine
     17 
     18 
     19 class 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()
     39 
     40     def setpat(self, pat):
     41         self.patvar.set(pat)
     42 
     43     def isre(self):
     44         return self.revar.get()
     45 
     46     def iscase(self):
     47         return self.casevar.get()
     48 
     49     def isword(self):
     50         return self.wordvar.get()
     51 
     52     def iswrap(self):
     53         return self.wrapvar.get()
     54 
     55     def isback(self):
     56         return self.backvar.get()
     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():
     64             pat = re.escape(pat)
     65         self.setpat(pat)
     66 
     67     def getcookedpat(self):
     68         pat = self.getpat()
     69         if not self.isre():  # if True, see setcookedpat
     70             pat = re.escape(pat)
     71         if self.isword():
     72             pat = r"\b%s\b" % pat
     73         return pat
     74 
     75     def getprog(self):
     76         "Return compiled cooked search pattern."
     77         pat = self.getpat()
     78         if not pat:
     79             self.report_error(pat, "Empty regular expression")
     80             return None
     81         pat = self.getcookedpat()
     82         flags = 0
     83         if not self.iscase():
     84             flags = flags | re.IGNORECASE
     85         try:
     86             prog = re.compile(pat, flags)
     87         except re.error as what:
     88             args = what.args
     89             msg = args[0]
     90             col = args[1] if len(args) >= 2 else -1
     91             self.report_error(pat, msg, col)
     92             return None
     93         return prog
     94 
     95     def report_error(self, pat, msg, col=-1):
     96         # Derived class could override this with something fancier
     97         msg = "Error: " + str(msg)
     98         if pat:
     99             msg = msg + "\nPattern: " + str(pat)
    100         if col >= 0:
    101             msg = msg + "\nOffset: " + str(col)
    102         tkMessageBox.showerror("Regular expression error",
    103                                msg, master=self.root)
    104 
    105     def search_text(self, text, prog=None, ok=0):
    106         '''Return (lineno, matchobj) or None for forward/backward search.
    107 
    108         This function calls the right function with the right arguments.
    109         It directly return the result of that call.
    110 
    111         Text is a text widget. Prog is a precompiled pattern.
    112         The ok parameter is a bit complicated as it has two effects.
    113 
    114         If there is a selection, the search begin at either end,
    115         depending on the direction setting and ok, with ok meaning that
    116         the search starts with the selection. Otherwise, search begins
    117         at the insert mark.
    118 
    119         To aid progress, the search functions do not return an empty
    120         match at the starting position unless ok is True.
    121         '''
    122 
    123         if not prog:
    124             prog = self.getprog()
    125             if not prog:
    126                 return None # Compilation failed -- stop
    127         wrap = self.wrapvar.get()
    128         first, last = get_selection(text)
    129         if self.isback():
    130             if ok:
    131                 start = last
    132             else:
    133                 start = first
    134             line, col = get_line_col(start)
    135             res = self.search_backward(text, prog, line, col, wrap, ok)
    136         else:
    137             if ok:
    138                 start = first
    139             else:
    140                 start = last
    141             line, col = get_line_col(start)
    142             res = self.search_forward(text, prog, line, col, wrap, ok)
    143         return res
    144 
    145     def search_forward(self, text, prog, line, col, wrap, ok=0):
    146         wrapped = 0
    147         startline = line
    148         chars = text.get("%d.0" % line, "%d.0" % (line+1))
    149         while chars:
    150             m = prog.search(chars[:-1], col)
    151             if m:
    152                 if ok or m.end() > col:
    153                     return line, m
    154             line = line + 1
    155             if wrapped and line > startline:
    156                 break
    157             col = 0
    158             ok = 1
    159             chars = text.get("%d.0" % line, "%d.0" % (line+1))
    160             if not chars and wrap:
    161                 wrapped = 1
    162                 wrap = 0
    163                 line = 1
    164                 chars = text.get("1.0", "2.0")
    165         return None
    166 
    167     def search_backward(self, text, prog, line, col, wrap, ok=0):
    168         wrapped = 0
    169         startline = line
    170         chars = text.get("%d.0" % line, "%d.0" % (line+1))
    171         while 1:
    172             m = search_reverse(prog, chars[:-1], col)
    173             if m:
    174                 if ok or m.start() < col:
    175                     return line, m
    176             line = line - 1
    177             if wrapped and line < startline:
    178                 break
    179             ok = 1
    180             if line <= 0:
    181                 if not wrap:
    182                     break
    183                 wrapped = 1
    184                 wrap = 0
    185                 pos = text.index("end-1c")
    186                 line, col = map(int, pos.split("."))
    187             chars = text.get("%d.0" % line, "%d.0" % (line+1))
    188             col = len(chars) - 1
    189         return None
    190 
    191 
    192 def search_reverse(prog, chars, col):
    193     '''Search backwards and return an re match object or None.
    194 
    195     This is done by searching forwards until there is no match.
    196     Prog: compiled re object with a search method returning a match.
    197     Chars: line of text, without \\n.
    198     Col: stop index for the search; the limit for match.end().
    199     '''
    200     m = prog.search(chars)
    201     if not m:
    202         return None
    203     found = None
    204     i, j = m.span()  # m.start(), m.end() == match slice indexes
    205     while i < col and j <= col:
    206         found = m
    207         if i == j:
    208             j = j+1
    209         m = prog.search(chars, j)
    210         if not m:
    211             break
    212         i, j = m.span()
    213     return found
    214 
    215 def get_selection(text):
    216     '''Return tuple of 'line.col' indexes from selection or insert mark.
    217     '''
    218     try:
    219         first = text.index("sel.first")
    220         last = text.index("sel.last")
    221     except TclError:
    222         first = last = None
    223     if not first:
    224         first = text.index("insert")
    225     if not last:
    226         last = first
    227     return first, last
    228 
    229 def get_line_col(index):
    230     '''Return (line, col) tuple of ints from 'line.col' string.'''
    231     line, col = map(int, index.split(".")) # Fails on invalid index
    232     return line, col
    233 
    234 if __name__ == "__main__":
    235     import unittest
    236     unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
    237