Home | History | Annotate | Download | only in idlelib
      1 '''Define SearchEngine for search dialogs.'''
      2 import re
      3 from Tkinter import StringVar, BooleanVar, TclError
      4 import tkMessageBox
      5 
      6 def get(root):
      7     '''Return the singleton SearchEngine instance for the process.
      8 
      9     The single SearchEngine saves settings between dialog instances.
     10     If there is not a SearchEngine already, make one.
     11     '''
     12     if not hasattr(root, "_searchengine"):
     13         root._searchengine = SearchEngine(root)
     14         # This creates a cycle that persists until root is deleted.
     15     return root._searchengine
     16 
     17 class SearchEngine:
     18     """Handles searching a text widget for Find, Replace, and Grep."""
     19 
     20     def __init__(self, root):
     21         '''Initialize Variables that save search state.
     22 
     23         The dialogs bind these to the UI elements present in the dialogs.
     24         '''
     25         self.root = root  # need for report_error()
     26         self.patvar = StringVar(root, '')   # search pattern
     27         self.revar = BooleanVar(root, False)   # regular expression?
     28         self.casevar = BooleanVar(root, False)   # match case?
     29         self.wordvar = BooleanVar(root, False)   # match whole word?
     30         self.wrapvar = BooleanVar(root, True)   # wrap around buffer?
     31         self.backvar = BooleanVar(root, False)   # search backwards?
     32 
     33     # Access methods
     34 
     35     def getpat(self):
     36         return self.patvar.get()
     37 
     38     def setpat(self, pat):
     39         self.patvar.set(pat)
     40 
     41     def isre(self):
     42         return self.revar.get()
     43 
     44     def iscase(self):
     45         return self.casevar.get()
     46 
     47     def isword(self):
     48         return self.wordvar.get()
     49 
     50     def iswrap(self):
     51         return self.wrapvar.get()
     52 
     53     def isback(self):
     54         return self.backvar.get()
     55 
     56     # Higher level access methods
     57 
     58     def setcookedpat(self, pat):
     59         "Set pattern after escaping if re."
     60         # called only in SearchDialog.py: 66
     61         if self.isre():
     62             pat = re.escape(pat)
     63         self.setpat(pat)
     64 
     65     def getcookedpat(self):
     66         pat = self.getpat()
     67         if not self.isre():  # if True, see setcookedpat
     68             pat = re.escape(pat)
     69         if self.isword():
     70             pat = r"\b%s\b" % pat
     71         return pat
     72 
     73     def getprog(self):
     74         "Return compiled cooked search pattern."
     75         pat = self.getpat()
     76         if not pat:
     77             self.report_error(pat, "Empty regular expression")
     78             return None
     79         pat = self.getcookedpat()
     80         flags = 0
     81         if not self.iscase():
     82             flags = flags | re.IGNORECASE
     83         try:
     84             prog = re.compile(pat, flags)
     85         except re.error as what:
     86             args = what.args
     87             msg = args[0]
     88             col = args[1] if len(args) >= 2 else -1
     89             self.report_error(pat, msg, col)
     90             return None
     91         return prog
     92 
     93     def report_error(self, pat, msg, col=-1):
     94         # Derived class could override this with something fancier
     95         msg = "Error: " + str(msg)
     96         if pat:
     97             msg = msg + "\nPattern: " + str(pat)
     98         if col >= 0:
     99             msg = msg + "\nOffset: " + str(col)
    100         tkMessageBox.showerror("Regular expression error",
    101                                msg, master=self.root)
    102 
    103     def search_text(self, text, prog=None, ok=0):
    104         '''Return (lineno, matchobj) or None for forward/backward search.
    105 
    106         This function calls the right function with the right arguments.
    107         It directly return the result of that call.
    108 
    109         Text is a text widget. Prog is a precompiled pattern.
    110         The ok parameter is a bit complicated as it has two effects.
    111 
    112         If there is a selection, the search begin at either end,
    113         depending on the direction setting and ok, with ok meaning that
    114         the search starts with the selection. Otherwise, search begins
    115         at the insert mark.
    116 
    117         To aid progress, the search functions do not return an empty
    118         match at the starting position unless ok is True.
    119         '''
    120 
    121         if not prog:
    122             prog = self.getprog()
    123             if not prog:
    124                 return None # Compilation failed -- stop
    125         wrap = self.wrapvar.get()
    126         first, last = get_selection(text)
    127         if self.isback():
    128             if ok:
    129                 start = last
    130             else:
    131                 start = first
    132             line, col = get_line_col(start)
    133             res = self.search_backward(text, prog, line, col, wrap, ok)
    134         else:
    135             if ok:
    136                 start = first
    137             else:
    138                 start = last
    139             line, col = get_line_col(start)
    140             res = self.search_forward(text, prog, line, col, wrap, ok)
    141         return res
    142 
    143     def search_forward(self, text, prog, line, col, wrap, ok=0):
    144         wrapped = 0
    145         startline = line
    146         chars = text.get("%d.0" % line, "%d.0" % (line+1))
    147         while chars:
    148             m = prog.search(chars[:-1], col)
    149             if m:
    150                 if ok or m.end() > col:
    151                     return line, m
    152             line = line + 1
    153             if wrapped and line > startline:
    154                 break
    155             col = 0
    156             ok = 1
    157             chars = text.get("%d.0" % line, "%d.0" % (line+1))
    158             if not chars and wrap:
    159                 wrapped = 1
    160                 wrap = 0
    161                 line = 1
    162                 chars = text.get("1.0", "2.0")
    163         return None
    164 
    165     def search_backward(self, text, prog, line, col, wrap, ok=0):
    166         wrapped = 0
    167         startline = line
    168         chars = text.get("%d.0" % line, "%d.0" % (line+1))
    169         while 1:
    170             m = search_reverse(prog, chars[:-1], col)
    171             if m:
    172                 if ok or m.start() < col:
    173                     return line, m
    174             line = line - 1
    175             if wrapped and line < startline:
    176                 break
    177             ok = 1
    178             if line <= 0:
    179                 if not wrap:
    180                     break
    181                 wrapped = 1
    182                 wrap = 0
    183                 pos = text.index("end-1c")
    184                 line, col = map(int, pos.split("."))
    185             chars = text.get("%d.0" % line, "%d.0" % (line+1))
    186             col = len(chars) - 1
    187         return None
    188 
    189 def 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)
    198     if not m:
    199         return None
    200     found = None
    201     i, j = m.span()  # m.start(), m.end() == match slice indexes
    202     while i < col and j <= col:
    203         found = m
    204         if i == j:
    205             j = j+1
    206         m = prog.search(chars, j)
    207         if not m:
    208             break
    209         i, j = m.span()
    210     return found
    211 
    212 def get_selection(text):
    213     '''Return tuple of 'line.col' indexes from selection or insert mark.
    214     '''
    215     try:
    216         first = text.index("sel.first")
    217         last = text.index("sel.last")
    218     except TclError:
    219         first = last = None
    220     if not first:
    221         first = text.index("insert")
    222     if not last:
    223         last = first
    224     return first, last
    225 
    226 def 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
    229     return line, col
    230 
    231 if __name__ == "__main__":
    232     import unittest
    233     unittest.main('idlelib.idle_test.test_searchengine', verbosity=2, exit=False)
    234