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