define searchengine for search dialogs import re from tkinter import stringvar booleanvar tclerror from tkinter import messagebox def getroot if not hasattrroot searchengine root searchengine searchengineroot this creates a cycle that persists until root is deleted return root searchengine class searchengine initialize variables that save search state the dialogs bind these to the ui elements present in the dialogs access methods higher level access methods called only in search py 66 derived class could override this with something fancier return lineno matchobj or none for forwardbackward search this function calls the right function with the right arguments it directly return the result of that call text is a text widget prog is a precompiled pattern the ok parameter is a bit complicated as it has two effects 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 to aid progress the search functions do not return an empty match at the starting position unless ok is true search backwards and return an re match object or none this is done by searching forwards until there is no match prog compiled re object with a search method returning a match chars line of text without n col stop index for the search the limit for match end return tuple of line col indexes from selection or insert mark return line col tuple of ints from line col string line col mapint index split fails on invalid index return line col if name main from unittest import main main idlelib idletest testsearchengine verbosity2 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 this creates a cycle that persists until root is deleted handles searching a text widget for find replace and grep initialize variables that save search state the dialogs bind these to the ui elements present in the dialogs need for report_error search pattern regular expression match case match whole word wrap around buffer search backwards access methods higher level access methods called only in search py 66 if true see setcookedpat derived class could override this with something fancier 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 text is a text widget prog is a precompiled pattern the ok parameter is a bit complicated as it has two effects 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 to aid progress the search functions do not return an empty match at the starting position unless ok is true compilation failed stop search backwards and return an re match object or none this is done by searching forwards until there is no match prog compiled re object with a search method returning a match chars line of text without n col stop index for the search the limit for match end m start m end match slice indexes return tuple of line col indexes from selection or insert mark return line col tuple of ints from line col string fails on invalid index
import re

from tkinter import StringVar, BooleanVar, TclError
from tkinter import messagebox

def get(root):
    
    if not hasattr(root, "_searchengine"):
        root._searchengine = SearchEngine(root)
        
    return root._searchengine


class SearchEngine:
    

    def __init__(self, root):
        
        self.root = root  
        self.patvar = StringVar(root, '')   
        self.revar = BooleanVar(root, False)   
        self.casevar = BooleanVar(root, False)   
        self.wordvar = BooleanVar(root, False)   
        self.wrapvar = BooleanVar(root, True)   
        self.backvar = BooleanVar(root, False)   

    

    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()

    

    def setcookedpat(self, pat):
        "Set pattern after escaping if re."
        
        if self.isre():
            pat = re.escape(pat)
        self.setpat(pat)

    def getcookedpat(self):
        pat = self.getpat()
        if not self.isre():  
            pat = re.escape(pat)
        if self.isword():
            pat = r"\b%s\b" % pat
        return pat

    def getprog(self):
        "Return compiled cooked search pattern."
        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)
        except re.error as e:
            self.report_error(pat, e.msg, e.pos)
            return None
        return prog

    def report_error(self, pat, msg, col=None):
        
        msg = "Error: " + str(msg)
        if pat:
            msg = msg + "\nPattern: " + str(pat)
        if col is not None:
            msg = msg + "\nOffset: " + str(col)
        messagebox.showerror("Regular expression error",
                               msg, master=self.root)

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

        if not prog:
            prog = self.getprog()
            if not prog:
                return None 
        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))
        while True:
            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")
                line, col = map(int, pos.split("."))
            chars = text.get("%d.0" % line, "%d.0" % (line+1))
            col = len(chars) - 1
        return None


def search_reverse(prog, chars, col):
    
    m = prog.search(chars)
    if not m:
        return None
    found = None
    i, j = m.span()  
    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):
    
    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):
    
    line, col = map(int, index.split(".")) 
    return line, col


if __name__ == "__main__":
    from unittest import main
    main('idlelib.idle_test.test_searchengine', verbosity=2)