(root)/
Python-3.12.0/
Lib/
idlelib/
searchengine.py
       1  '''Define SearchEngine for search dialogs.'''
       2  import re
       3  
       4  from tkinter import StringVar, BooleanVar, TclError
       5  from tkinter import messagebox
       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 ESC[4;38;5;81mSearchEngine:
      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 e:
      88              self.report_error(pat, e.msg, e.pos)
      89              return None
      90          return prog
      91  
      92      def report_error(self, pat, msg, col=None):
      93          # Derived class could override this with something fancier
      94          msg = "Error: " + str(msg)
      95          if pat:
      96              msg = msg + "\nPattern: " + str(pat)
      97          if col is not None:
      98              msg = msg + "\nOffset: " + str(col)
      99          messagebox.showerror("Regular expression error",
     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:
     121              prog = self.getprog()
     122              if not prog:
     123                  return None # Compilation failed -- stop
     124          wrap = self.wrapvar.get()
     125          first, last = get_selection(text)
     126          if self.isback():
     127              if ok:
     128                  start = last
     129              else:
     130                  start = first
     131              line, col = get_line_col(start)
     132              res = self.search_backward(text, prog, line, col, wrap, ok)
     133          else:
     134              if ok:
     135                  start = first
     136              else:
     137                  start = last
     138              line, col = get_line_col(start)
     139              res = self.search_forward(text, prog, line, col, wrap, ok)
     140          return res
     141  
     142      def search_forward(self, text, prog, line, col, wrap, ok=0):
     143          wrapped = 0
     144          startline = line
     145          chars = text.get("%d.0" % line, "%d.0" % (line+1))
     146          while chars:
     147              m = prog.search(chars[:-1], col)
     148              if m:
     149                  if ok or m.end() > col:
     150                      return line, m
     151              line = line + 1
     152              if wrapped and line > startline:
     153                  break
     154              col = 0
     155              ok = 1
     156              chars = text.get("%d.0" % line, "%d.0" % (line+1))
     157              if not chars and wrap:
     158                  wrapped = 1
     159                  wrap = 0
     160                  line = 1
     161                  chars = text.get("1.0", "2.0")
     162          return None
     163  
     164      def search_backward(self, text, prog, line, col, wrap, ok=0):
     165          wrapped = 0
     166          startline = line
     167          chars = text.get("%d.0" % line, "%d.0" % (line+1))
     168          while True:
     169              m = search_reverse(prog, chars[:-1], col)
     170              if m:
     171                  if ok or m.start() < col:
     172                      return line, m
     173              line = line - 1
     174              if wrapped and line < startline:
     175                  break
     176              ok = 1
     177              if line <= 0:
     178                  if not wrap:
     179                      break
     180                  wrapped = 1
     181                  wrap = 0
     182                  pos = text.index("end-1c")
     183                  line, col = map(int, pos.split("."))
     184              chars = text.get("%d.0" % line, "%d.0" % (line+1))
     185              col = len(chars) - 1
     186          return None
     187  
     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  
     232  if __name__ == "__main__":
     233      from unittest import main
     234      main('idlelib.idle_test.test_searchengine', verbosity=2)