17db96d56Sopenharmony_ci"""Grep dialog for Find in Files functionality.
27db96d56Sopenharmony_ci
37db96d56Sopenharmony_ci   Inherits from SearchDialogBase for GUI and uses searchengine
47db96d56Sopenharmony_ci   to prepare search pattern.
57db96d56Sopenharmony_ci"""
67db96d56Sopenharmony_ciimport fnmatch
77db96d56Sopenharmony_ciimport os
87db96d56Sopenharmony_ciimport sys
97db96d56Sopenharmony_ci
107db96d56Sopenharmony_cifrom tkinter import StringVar, BooleanVar
117db96d56Sopenharmony_cifrom tkinter.ttk import Checkbutton  # Frame imported in ...Base
127db96d56Sopenharmony_ci
137db96d56Sopenharmony_cifrom idlelib.searchbase import SearchDialogBase
147db96d56Sopenharmony_cifrom idlelib import searchengine
157db96d56Sopenharmony_ci
167db96d56Sopenharmony_ci# Importing OutputWindow here fails due to import loop
177db96d56Sopenharmony_ci# EditorWindow -> GrepDialog -> OutputWindow -> EditorWindow
187db96d56Sopenharmony_ci
197db96d56Sopenharmony_ci
207db96d56Sopenharmony_cidef grep(text, io=None, flist=None):
217db96d56Sopenharmony_ci    """Open the Find in Files dialog.
227db96d56Sopenharmony_ci
237db96d56Sopenharmony_ci    Module-level function to access the singleton GrepDialog
247db96d56Sopenharmony_ci    instance and open the dialog.  If text is selected, it is
257db96d56Sopenharmony_ci    used as the search phrase; otherwise, the previous entry
267db96d56Sopenharmony_ci    is used.
277db96d56Sopenharmony_ci
287db96d56Sopenharmony_ci    Args:
297db96d56Sopenharmony_ci        text: Text widget that contains the selected text for
307db96d56Sopenharmony_ci              default search phrase.
317db96d56Sopenharmony_ci        io: iomenu.IOBinding instance with default path to search.
327db96d56Sopenharmony_ci        flist: filelist.FileList instance for OutputWindow parent.
337db96d56Sopenharmony_ci    """
347db96d56Sopenharmony_ci    root = text._root()
357db96d56Sopenharmony_ci    engine = searchengine.get(root)
367db96d56Sopenharmony_ci    if not hasattr(engine, "_grepdialog"):
377db96d56Sopenharmony_ci        engine._grepdialog = GrepDialog(root, engine, flist)
387db96d56Sopenharmony_ci    dialog = engine._grepdialog
397db96d56Sopenharmony_ci    searchphrase = text.get("sel.first", "sel.last")
407db96d56Sopenharmony_ci    dialog.open(text, searchphrase, io)
417db96d56Sopenharmony_ci
427db96d56Sopenharmony_ci
437db96d56Sopenharmony_cidef walk_error(msg):
447db96d56Sopenharmony_ci    "Handle os.walk error."
457db96d56Sopenharmony_ci    print(msg)
467db96d56Sopenharmony_ci
477db96d56Sopenharmony_ci
487db96d56Sopenharmony_cidef findfiles(folder, pattern, recursive):
497db96d56Sopenharmony_ci    """Generate file names in dir that match pattern.
507db96d56Sopenharmony_ci
517db96d56Sopenharmony_ci    Args:
527db96d56Sopenharmony_ci        folder: Root directory to search.
537db96d56Sopenharmony_ci        pattern: File pattern to match.
547db96d56Sopenharmony_ci        recursive: True to include subdirectories.
557db96d56Sopenharmony_ci    """
567db96d56Sopenharmony_ci    for dirpath, _, filenames in os.walk(folder, onerror=walk_error):
577db96d56Sopenharmony_ci        yield from (os.path.join(dirpath, name)
587db96d56Sopenharmony_ci                    for name in filenames
597db96d56Sopenharmony_ci                    if fnmatch.fnmatch(name, pattern))
607db96d56Sopenharmony_ci        if not recursive:
617db96d56Sopenharmony_ci            break
627db96d56Sopenharmony_ci
637db96d56Sopenharmony_ci
647db96d56Sopenharmony_ciclass GrepDialog(SearchDialogBase):
657db96d56Sopenharmony_ci    "Dialog for searching multiple files."
667db96d56Sopenharmony_ci
677db96d56Sopenharmony_ci    title = "Find in Files Dialog"
687db96d56Sopenharmony_ci    icon = "Grep"
697db96d56Sopenharmony_ci    needwrapbutton = 0
707db96d56Sopenharmony_ci
717db96d56Sopenharmony_ci    def __init__(self, root, engine, flist):
727db96d56Sopenharmony_ci        """Create search dialog for searching for a phrase in the file system.
737db96d56Sopenharmony_ci
747db96d56Sopenharmony_ci        Uses SearchDialogBase as the basis for the GUI and a
757db96d56Sopenharmony_ci        searchengine instance to prepare the search.
767db96d56Sopenharmony_ci
777db96d56Sopenharmony_ci        Attributes:
787db96d56Sopenharmony_ci            flist: filelist.Filelist instance for OutputWindow parent.
797db96d56Sopenharmony_ci            globvar: String value of Entry widget for path to search.
807db96d56Sopenharmony_ci            globent: Entry widget for globvar.  Created in
817db96d56Sopenharmony_ci                create_entries().
827db96d56Sopenharmony_ci            recvar: Boolean value of Checkbutton widget for
837db96d56Sopenharmony_ci                traversing through subdirectories.
847db96d56Sopenharmony_ci        """
857db96d56Sopenharmony_ci        super().__init__(root, engine)
867db96d56Sopenharmony_ci        self.flist = flist
877db96d56Sopenharmony_ci        self.globvar = StringVar(root)
887db96d56Sopenharmony_ci        self.recvar = BooleanVar(root)
897db96d56Sopenharmony_ci
907db96d56Sopenharmony_ci    def open(self, text, searchphrase, io=None):
917db96d56Sopenharmony_ci        """Make dialog visible on top of others and ready to use.
927db96d56Sopenharmony_ci
937db96d56Sopenharmony_ci        Extend the SearchDialogBase open() to set the initial value
947db96d56Sopenharmony_ci        for globvar.
957db96d56Sopenharmony_ci
967db96d56Sopenharmony_ci        Args:
977db96d56Sopenharmony_ci            text: Multicall object containing the text information.
987db96d56Sopenharmony_ci            searchphrase: String phrase to search.
997db96d56Sopenharmony_ci            io: iomenu.IOBinding instance containing file path.
1007db96d56Sopenharmony_ci        """
1017db96d56Sopenharmony_ci        SearchDialogBase.open(self, text, searchphrase)
1027db96d56Sopenharmony_ci        if io:
1037db96d56Sopenharmony_ci            path = io.filename or ""
1047db96d56Sopenharmony_ci        else:
1057db96d56Sopenharmony_ci            path = ""
1067db96d56Sopenharmony_ci        dir, base = os.path.split(path)
1077db96d56Sopenharmony_ci        head, tail = os.path.splitext(base)
1087db96d56Sopenharmony_ci        if not tail:
1097db96d56Sopenharmony_ci            tail = ".py"
1107db96d56Sopenharmony_ci        self.globvar.set(os.path.join(dir, "*" + tail))
1117db96d56Sopenharmony_ci
1127db96d56Sopenharmony_ci    def create_entries(self):
1137db96d56Sopenharmony_ci        "Create base entry widgets and add widget for search path."
1147db96d56Sopenharmony_ci        SearchDialogBase.create_entries(self)
1157db96d56Sopenharmony_ci        self.globent = self.make_entry("In files:", self.globvar)[0]
1167db96d56Sopenharmony_ci
1177db96d56Sopenharmony_ci    def create_other_buttons(self):
1187db96d56Sopenharmony_ci        "Add check button to recurse down subdirectories."
1197db96d56Sopenharmony_ci        btn = Checkbutton(
1207db96d56Sopenharmony_ci                self.make_frame()[0], variable=self.recvar,
1217db96d56Sopenharmony_ci                text="Recurse down subdirectories")
1227db96d56Sopenharmony_ci        btn.pack(side="top", fill="both")
1237db96d56Sopenharmony_ci
1247db96d56Sopenharmony_ci    def create_command_buttons(self):
1257db96d56Sopenharmony_ci        "Create base command buttons and add button for Search Files."
1267db96d56Sopenharmony_ci        SearchDialogBase.create_command_buttons(self)
1277db96d56Sopenharmony_ci        self.make_button("Search Files", self.default_command, isdef=True)
1287db96d56Sopenharmony_ci
1297db96d56Sopenharmony_ci    def default_command(self, event=None):
1307db96d56Sopenharmony_ci        """Grep for search pattern in file path. The default command is bound
1317db96d56Sopenharmony_ci        to <Return>.
1327db96d56Sopenharmony_ci
1337db96d56Sopenharmony_ci        If entry values are populated, set OutputWindow as stdout
1347db96d56Sopenharmony_ci        and perform search.  The search dialog is closed automatically
1357db96d56Sopenharmony_ci        when the search begins.
1367db96d56Sopenharmony_ci        """
1377db96d56Sopenharmony_ci        prog = self.engine.getprog()
1387db96d56Sopenharmony_ci        if not prog:
1397db96d56Sopenharmony_ci            return
1407db96d56Sopenharmony_ci        path = self.globvar.get()
1417db96d56Sopenharmony_ci        if not path:
1427db96d56Sopenharmony_ci            self.top.bell()
1437db96d56Sopenharmony_ci            return
1447db96d56Sopenharmony_ci        from idlelib.outwin import OutputWindow  # leave here!
1457db96d56Sopenharmony_ci        save = sys.stdout
1467db96d56Sopenharmony_ci        try:
1477db96d56Sopenharmony_ci            sys.stdout = OutputWindow(self.flist)
1487db96d56Sopenharmony_ci            self.grep_it(prog, path)
1497db96d56Sopenharmony_ci        finally:
1507db96d56Sopenharmony_ci            sys.stdout = save
1517db96d56Sopenharmony_ci
1527db96d56Sopenharmony_ci    def grep_it(self, prog, path):
1537db96d56Sopenharmony_ci        """Search for prog within the lines of the files in path.
1547db96d56Sopenharmony_ci
1557db96d56Sopenharmony_ci        For the each file in the path directory, open the file and
1567db96d56Sopenharmony_ci        search each line for the matching pattern.  If the pattern is
1577db96d56Sopenharmony_ci        found,  write the file and line information to stdout (which
1587db96d56Sopenharmony_ci        is an OutputWindow).
1597db96d56Sopenharmony_ci
1607db96d56Sopenharmony_ci        Args:
1617db96d56Sopenharmony_ci            prog: The compiled, cooked search pattern.
1627db96d56Sopenharmony_ci            path: String containing the search path.
1637db96d56Sopenharmony_ci        """
1647db96d56Sopenharmony_ci        folder, filepat = os.path.split(path)
1657db96d56Sopenharmony_ci        if not folder:
1667db96d56Sopenharmony_ci            folder = os.curdir
1677db96d56Sopenharmony_ci        filelist = sorted(findfiles(folder, filepat, self.recvar.get()))
1687db96d56Sopenharmony_ci        self.close()
1697db96d56Sopenharmony_ci        pat = self.engine.getpat()
1707db96d56Sopenharmony_ci        print(f"Searching {pat!r} in {path} ...")
1717db96d56Sopenharmony_ci        hits = 0
1727db96d56Sopenharmony_ci        try:
1737db96d56Sopenharmony_ci            for fn in filelist:
1747db96d56Sopenharmony_ci                try:
1757db96d56Sopenharmony_ci                    with open(fn, errors='replace') as f:
1767db96d56Sopenharmony_ci                        for lineno, line in enumerate(f, 1):
1777db96d56Sopenharmony_ci                            if line[-1:] == '\n':
1787db96d56Sopenharmony_ci                                line = line[:-1]
1797db96d56Sopenharmony_ci                            if prog.search(line):
1807db96d56Sopenharmony_ci                                sys.stdout.write(f"{fn}: {lineno}: {line}\n")
1817db96d56Sopenharmony_ci                                hits += 1
1827db96d56Sopenharmony_ci                except OSError as msg:
1837db96d56Sopenharmony_ci                    print(msg)
1847db96d56Sopenharmony_ci            print(f"Hits found: {hits}\n(Hint: right-click to open locations.)"
1857db96d56Sopenharmony_ci                  if hits else "No hits.")
1867db96d56Sopenharmony_ci        except AttributeError:
1877db96d56Sopenharmony_ci            # Tk window has been closed, OutputWindow.text = None,
1887db96d56Sopenharmony_ci            # so in OW.write, OW.text.insert fails.
1897db96d56Sopenharmony_ci            pass
1907db96d56Sopenharmony_ci
1917db96d56Sopenharmony_ci
1927db96d56Sopenharmony_cidef _grep_dialog(parent):  # htest #
1937db96d56Sopenharmony_ci    from tkinter import Toplevel, Text, SEL, END
1947db96d56Sopenharmony_ci    from tkinter.ttk import Frame, Button
1957db96d56Sopenharmony_ci    from idlelib.pyshell import PyShellFileList
1967db96d56Sopenharmony_ci
1977db96d56Sopenharmony_ci    top = Toplevel(parent)
1987db96d56Sopenharmony_ci    top.title("Test GrepDialog")
1997db96d56Sopenharmony_ci    x, y = map(int, parent.geometry().split('+')[1:])
2007db96d56Sopenharmony_ci    top.geometry(f"+{x}+{y + 175}")
2017db96d56Sopenharmony_ci
2027db96d56Sopenharmony_ci    flist = PyShellFileList(top)
2037db96d56Sopenharmony_ci    frame = Frame(top)
2047db96d56Sopenharmony_ci    frame.pack()
2057db96d56Sopenharmony_ci    text = Text(frame, height=5)
2067db96d56Sopenharmony_ci    text.pack()
2077db96d56Sopenharmony_ci
2087db96d56Sopenharmony_ci    def show_grep_dialog():
2097db96d56Sopenharmony_ci        text.tag_add(SEL, "1.0", END)
2107db96d56Sopenharmony_ci        grep(text, flist=flist)
2117db96d56Sopenharmony_ci        text.tag_remove(SEL, "1.0", END)
2127db96d56Sopenharmony_ci
2137db96d56Sopenharmony_ci    button = Button(frame, text="Show GrepDialog", command=show_grep_dialog)
2147db96d56Sopenharmony_ci    button.pack()
2157db96d56Sopenharmony_ci
2167db96d56Sopenharmony_ciif __name__ == "__main__":
2177db96d56Sopenharmony_ci    from unittest import main
2187db96d56Sopenharmony_ci    main('idlelib.idle_test.test_grep', verbosity=2, exit=False)
2197db96d56Sopenharmony_ci
2207db96d56Sopenharmony_ci    from idlelib.idle_test.htest import run
2217db96d56Sopenharmony_ci    run(_grep_dialog)
222