--- a/eric7/UI/FindFileDialog.py Fri Sep 17 19:53:54 2021 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,778 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright (c) 2002 - 2021 Detlev Offenbach <detlev@die-offenbachs.de> -# - -""" -Module implementing a dialog to search for text in files. -""" - -import os -import re - -from PyQt6.QtCore import pyqtSignal, Qt, pyqtSlot -from PyQt6.QtGui import QCursor -from PyQt6.QtWidgets import ( - QDialog, QApplication, QMenu, QDialogButtonBox, QTreeWidgetItem, QComboBox -) - -from EricWidgets.EricApplication import ericApp -from EricWidgets import EricMessageBox -from EricWidgets.EricPathPicker import EricPathPickerModes - -from .Ui_FindFileDialog import Ui_FindFileDialog - -import Utilities -import Preferences - - -class FindFileDialog(QDialog, Ui_FindFileDialog): - """ - Class implementing a dialog to search for text in files. - - The occurrences found are displayed in a QTreeWidget showing the filename, - the linenumber and the found text. The file will be opened upon a double - click onto the respective entry of the list. - - @signal sourceFile(str, int, str, int, int) emitted to open a source file - at a line - @signal designerFile(str) emitted to open a Qt-Designer file - """ - sourceFile = pyqtSignal(str, int, str, int, int) - designerFile = pyqtSignal(str) - - lineRole = Qt.ItemDataRole.UserRole + 1 - startRole = Qt.ItemDataRole.UserRole + 2 - endRole = Qt.ItemDataRole.UserRole + 3 - replaceRole = Qt.ItemDataRole.UserRole + 4 - md5Role = Qt.ItemDataRole.UserRole + 5 - - def __init__(self, project, replaceMode=False, parent=None): - """ - Constructor - - @param project reference to the project object - @param replaceMode flag indicating the replace dialog mode (boolean) - @param parent parent widget of this dialog (QWidget) - """ - super().__init__(parent) - self.setupUi(self) - self.setWindowFlags(Qt.WindowType.Window) - - self.dirPicker.setMode(EricPathPickerModes.DIRECTORY_MODE) - self.dirPicker.setInsertPolicy(QComboBox.InsertPolicy.InsertAtTop) - self.dirPicker.setSizeAdjustPolicy( - QComboBox.SizeAdjustPolicy.AdjustToMinimumContentsLengthWithIcon) - - self.__replaceMode = replaceMode - - self.stopButton = self.buttonBox.addButton( - self.tr("Stop"), QDialogButtonBox.ButtonRole.ActionRole) - self.stopButton.setEnabled(False) - - self.findButton = self.buttonBox.addButton( - self.tr("Find"), QDialogButtonBox.ButtonRole.ActionRole) - self.findButton.setEnabled(False) - self.findButton.setDefault(True) - - if self.__replaceMode: - self.replaceButton.setEnabled(False) - self.setWindowTitle(self.tr("Replace in Files")) - else: - self.replaceLabel.hide() - self.replacetextCombo.hide() - self.replaceButton.hide() - - self.findProgressLabel.setMaximumWidth(550) - - self.findtextCombo.setCompleter(None) - self.replacetextCombo.setCompleter(None) - - self.searchHistory = Preferences.toList( - Preferences.Prefs.settings.value( - "FindFileDialog/SearchHistory")) - self.replaceHistory = Preferences.toList( - Preferences.Prefs.settings.value( - "FindFileDialog/ReplaceHistory")) - self.dirHistory = Preferences.toList( - Preferences.Prefs.settings.value( - "FindFileDialog/DirectoryHistory")) - self.findtextCombo.addItems(self.searchHistory) - self.replacetextCombo.addItems(self.replaceHistory) - self.dirPicker.addItems(self.dirHistory) - - self.excludeHiddenCheckBox.setChecked(Preferences.toBool( - Preferences.Prefs.settings.value( - "FindFileDialog/ExcludeHidden", True) - )) - - self.project = project - - self.findList.headerItem().setText(self.findList.columnCount(), "") - self.findList.header().setSortIndicator(0, Qt.SortOrder.AscendingOrder) - self.__section0Size = self.findList.header().sectionSize(0) - self.findList.setExpandsOnDoubleClick(False) - if self.__replaceMode: - font = Preferences.getEditorOtherFonts("MonospacedFont") - self.findList.setFont(font) - - # Qt Designer form files - self.filterForms = r'.*\.ui$' - self.formsExt = ['*.ui'] - - # Corba interface files - self.filterInterfaces = r'.*\.idl$' - self.interfacesExt = ['*.idl'] - - # Protobuf protocol files - self.filterProtocols = r'.*\.proto$' - self.protocolsExt = ['*.proto'] - - # Qt resources files - self.filterResources = r'.*\.qrc$' - self.resourcesExt = ['*.qrc'] - - self.__cancelSearch = False - self.__lastFileItem = None - self.__populating = False - - self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) - self.customContextMenuRequested.connect(self.__contextMenuRequested) - - def __createItem(self, file, line, text, start, end, replTxt="", md5=""): - """ - Private method to create an entry in the file list. - - @param file filename of file (string) - @param line line number (integer) - @param text text found (string) - @param start start position of match (integer) - @param end end position of match (integer) - @param replTxt text with replacements applied (string) - @param md5 MD5 hash of the file (string) - """ - if self.__lastFileItem is None: - # It's a new file - self.__lastFileItem = QTreeWidgetItem(self.findList, [file]) - self.__lastFileItem.setFirstColumnSpanned(True) - self.__lastFileItem.setExpanded(True) - if self.__replaceMode: - self.__lastFileItem.setFlags( - self.__lastFileItem.flags() | - Qt.ItemFlag.ItemIsUserCheckable | - Qt.ItemFlag.ItemIsAutoTristate) - self.__lastFileItem.setData(0, self.md5Role, md5) - - itm = QTreeWidgetItem(self.__lastFileItem) - itm.setTextAlignment(0, Qt.AlignmentFlag.AlignRight) - itm.setData(0, Qt.ItemDataRole.DisplayRole, line) - itm.setData(1, Qt.ItemDataRole.DisplayRole, text) - itm.setData(0, self.lineRole, line) - itm.setData(0, self.startRole, start) - itm.setData(0, self.endRole, end) - itm.setData(0, self.replaceRole, replTxt) - if self.__replaceMode: - itm.setFlags(itm.flags() | - Qt.ItemFlag.ItemIsUserCheckable) - itm.setCheckState(0, Qt.CheckState.Checked) - self.replaceButton.setEnabled(True) - - def show(self, txt=""): - """ - Public method to enable/disable the project button. - - @param txt text to be shown in the searchtext combo (string) - """ - if self.project and self.project.isOpen(): - self.projectButton.setEnabled(True) - else: - self.projectButton.setEnabled(False) - self.dirButton.setChecked(True) - - self.findtextCombo.setEditText(txt) - self.findtextCombo.lineEdit().selectAll() - self.findtextCombo.setFocus() - - if self.__replaceMode: - self.findList.clear() - self.replacetextCombo.setEditText("") - - super().show() - - def on_findtextCombo_editTextChanged(self, text): - """ - Private slot to handle the editTextChanged signal of the find - text combo. - - @param text (ignored) - """ - self.__enableFindButton() - - def on_replacetextCombo_editTextChanged(self, text): - """ - Private slot to handle the editTextChanged signal of the replace - text combo. - - @param text (ignored) - """ - self.__enableFindButton() - - def on_dirPicker_editTextChanged(self, text): - """ - Private slot to handle the textChanged signal of the directory - picker. - - @param text (ignored) - """ - self.__enableFindButton() - - @pyqtSlot() - def on_projectButton_clicked(self): - """ - Private slot to handle the selection of the 'Project' radio button. - """ - self.__enableFindButton() - - @pyqtSlot() - def on_dirButton_clicked(self): - """ - Private slot to handle the selection of the 'Directory' radio button. - """ - self.__enableFindButton() - - @pyqtSlot() - def on_openFilesButton_clicked(self): - """ - Private slot to handle the selection of the 'Open Files' radio button. - """ - self.__enableFindButton() - - @pyqtSlot() - def on_filterCheckBox_clicked(self): - """ - Private slot to handle the selection of the file filter check box. - """ - self.__enableFindButton() - - @pyqtSlot(str) - def on_filterEdit_textEdited(self, text): - """ - Private slot to handle the textChanged signal of the file filter edit. - - @param text (ignored) - """ - self.__enableFindButton() - - def __enableFindButton(self): - """ - Private slot called to enable the find button. - """ - if ( - self.findtextCombo.currentText() == "" or - (self.dirButton.isChecked() and - (self.dirPicker.currentText() == "" or - not os.path.exists(os.path.abspath( - self.dirPicker.currentText())))) or - (self.filterCheckBox.isChecked() and - self.filterEdit.text() == "") - ): - self.findButton.setEnabled(False) - self.buttonBox.button( - QDialogButtonBox.StandardButton.Close).setDefault(True) - else: - self.findButton.setEnabled(True) - self.findButton.setDefault(True) - - def on_buttonBox_clicked(self, button): - """ - Private slot called by a button of the button box clicked. - - @param button button that was clicked (QAbstractButton) - """ - if button == self.findButton: - self.__doSearch() - elif button == self.stopButton: - self.__stopSearch() - - def __stripEol(self, txt): - """ - Private method to strip the eol part. - - @param txt line of text that should be treated (string) - @return text with eol stripped (string) - """ - return txt.replace("\r", "").replace("\n", "") - - def __stopSearch(self): - """ - Private slot to handle the stop button being pressed. - """ - self.__cancelSearch = True - - def __doSearch(self): - """ - Private slot to handle the find button being pressed. - """ - if ( - self.__replaceMode and - not ericApp().getObject("ViewManager").checkAllDirty() - ): - return - - self.__cancelSearch = False - - if self.filterCheckBox.isChecked(): - fileFilter = self.filterEdit.text() - fileFilterList = [ - "^{0}$".format(filter.replace(".", r"\.").replace("*", ".*")) - for filter in fileFilter.split(";") - ] - filterRe = re.compile("|".join(fileFilterList)) - - if self.projectButton.isChecked(): - if self.filterCheckBox.isChecked(): - files = [ - self.project.getRelativePath(file) - for file in - self.__getFileList( - self.project.getProjectPath(), - filterRe, - excludeHiddenDirs=self.excludeHiddenCheckBox - .isChecked(), - ) - ] - else: - files = [] - if self.sourcesCheckBox.isChecked(): - files += self.project.pdata["SOURCES"] - if self.formsCheckBox.isChecked(): - files += self.project.pdata["FORMS"] - if self.interfacesCheckBox.isChecked(): - files += self.project.pdata["INTERFACES"] - if self.protocolsCheckBox.isChecked(): - files += self.project.pdata["PROTOCOLS"] - if self.resourcesCheckBox.isChecked(): - files += self.project.pdata["RESOURCES"] - elif self.dirButton.isChecked(): - if not self.filterCheckBox.isChecked(): - filters = [] - if ( - self.project.isOpen() and - os.path.abspath(self.dirPicker.currentText()).startswith( - self.project.getProjectPath()) - ): - if self.sourcesCheckBox.isChecked(): - filters.extend([ - "^{0}$".format( - assoc.replace(".", r"\.").replace("*", ".*") - ) for assoc in - self.project.getFiletypeAssociations("SOURCES") - ]) - if self.formsCheckBox.isChecked(): - filters.extend([ - "^{0}$".format( - assoc.replace(".", r"\.").replace("*", ".*") - ) for assoc in - self.project.getFiletypeAssociations("FORMS") - ]) - if self.interfacesCheckBox.isChecked(): - filters.extend([ - "^{0}$".format( - assoc.replace(".", r"\.").replace("*", ".*") - ) for assoc in - self.project.getFiletypeAssociations("INTERFACES") - ]) - if self.protocolsCheckBox.isChecked(): - filters.extend([ - "^{0}$".format( - assoc.replace(".", r"\.").replace("*", ".*") - ) for assoc in - self.project.getFiletypeAssociations("PROTOCOLS") - ]) - if self.resourcesCheckBox.isChecked(): - filters.extend([ - "^{0}$".format( - assoc.replace(".", r"\.").replace("*", ".*") - ) for assoc in - self.project.getFiletypeAssociations("RESOURCES") - ]) - else: - if self.sourcesCheckBox.isChecked(): - filters.extend([ - "^{0}$".format( - assoc.replace(".", r"\.").replace("*", ".*")) - for assoc in list( - Preferences.getEditorLexerAssocs().keys()) - if assoc not in - self.formsExt + self.interfacesExt + - self.protocolsExt + self.resourcesExt - ]) - if self.formsCheckBox.isChecked(): - filters.append(self.filterForms) - if self.interfacesCheckBox.isChecked(): - filters.append(self.filterInterfaces) - if self.protocolsCheckBox.isChecked(): - filters.append(self.filterProtocols) - if self.resourcesCheckBox.isChecked(): - filters.append(self.filterResources) - filterString = "|".join(filters) - filterRe = re.compile(filterString) - files = self.__getFileList( - os.path.abspath(self.dirPicker.currentText()), - filterRe, - excludeHiddenDirs=self.excludeHiddenCheckBox.isChecked(), - excludeHiddenFiles=self.excludeHiddenCheckBox.isChecked(), - ) - elif self.openFilesButton.isChecked(): - vm = ericApp().getObject("ViewManager") - vm.checkAllDirty() - files = vm.getOpenFilenames() - - self.findList.clear() - QApplication.processEvents() - QApplication.processEvents() - self.findProgress.setMaximum(len(files)) - - # retrieve the values - reg = self.regexpCheckBox.isChecked() - wo = self.wordCheckBox.isChecked() - cs = self.caseCheckBox.isChecked() - ct = self.findtextCombo.currentText() - txt = ct if reg else re.escape(ct) - if wo: - txt = "\\b{0}\\b".format(txt) - flags = re.UNICODE - if not cs: - flags |= re.IGNORECASE - try: - search = re.compile(txt, flags) - except re.error as why: - EricMessageBox.critical( - self, - self.tr("Invalid search expression"), - self.tr("""<p>The search expression is not valid.</p>""" - """<p>Error: {0}</p>""").format(str(why))) - self.stopButton.setEnabled(False) - self.findButton.setEnabled(True) - self.findButton.setDefault(True) - return - # reset the findtextCombo - if ct in self.searchHistory: - self.searchHistory.remove(ct) - self.searchHistory.insert(0, ct) - self.findtextCombo.clear() - self.findtextCombo.addItems(self.searchHistory) - Preferences.Prefs.settings.setValue( - "FindFileDialog/SearchHistory", - self.searchHistory[:30]) - Preferences.Prefs.settings.setValue( - "FindFileDialog/ExcludeHidden", - self.excludeHiddenCheckBox.isChecked()) - - if self.__replaceMode: - replTxt = self.replacetextCombo.currentText() - if replTxt in self.replaceHistory: - self.replaceHistory.remove(replTxt) - self.replaceHistory.insert(0, replTxt) - self.replacetextCombo.clear() - self.replacetextCombo.addItems(self.replaceHistory) - Preferences.Prefs.settings.setValue( - "FindFileDialog/ReplaceHistory", - self.replaceHistory[:30]) - - if self.dirButton.isChecked(): - searchDir = self.dirPicker.currentText() - if searchDir in self.dirHistory: - self.dirHistory.remove(searchDir) - self.dirHistory.insert(0, searchDir) - self.dirPicker.clear() - self.dirPicker.addItems(self.dirHistory) - Preferences.Prefs.settings.setValue( - "FindFileDialog/DirectoryHistory", - self.dirHistory[:30]) - - # set the button states - self.stopButton.setEnabled(True) - self.stopButton.setDefault(True) - self.findButton.setEnabled(False) - - # now go through all the files - self.__populating = True - self.findList.setUpdatesEnabled(False) - breakSearch = False - occurrences = 0 - fileOccurrences = 0 - for progress, file in enumerate(files, start=1): - self.__lastFileItem = None - found = False - if self.__cancelSearch or breakSearch: - break - - self.findProgressLabel.setPath(file) - - fn = ( - os.path.join(self.project.ppath, file) - if self.projectButton.isChecked() else - file - ) - # read the file and split it into textlines - try: - text, encoding, hashStr = Utilities.readEncodedFileWithHash(fn) - lines = text.splitlines(True) - except (UnicodeError, OSError): - self.findProgress.setValue(progress) - continue - - # now perform the search and display the lines found - for count, line in enumerate(lines, start=1): - if self.__cancelSearch: - break - - contains = search.search(line) - if contains: - occurrences += 1 - found = True - start = contains.start() - end = contains.end() - if self.__replaceMode: - rline = search.sub(replTxt, line) - else: - rline = "" - line = self.__stripEol(line) - if len(line) > 1024: - line = "{0} ...".format(line[:1024]) - if self.__replaceMode: - if len(rline) > 1024: - rline = "{0} ...".format(line[:1024]) - line = "- {0}\n+ {1}".format( - line, self.__stripEol(rline)) - self.__createItem(file, count, line, start, end, - rline, hashStr) - - if self.feelLikeCheckBox.isChecked(): - fn = os.path.join(self.project.ppath, file) - self.sourceFile.emit(fn, count, "", start, end) - QApplication.processEvents() - breakSearch = True - break - - QApplication.processEvents() - - if found: - fileOccurrences += 1 - self.findProgress.setValue(progress) - - if not files: - self.findProgress.setMaximum(1) - self.findProgress.setValue(1) - - resultFormat = self.tr("{0} / {1}", "occurrences / files") - self.findProgressLabel.setPath(resultFormat.format( - self.tr("%n occurrence(s)", "", occurrences), - self.tr("%n file(s)", "", fileOccurrences))) - - self.findList.setUpdatesEnabled(True) - self.findList.sortItems(self.findList.sortColumn(), - self.findList.header().sortIndicatorOrder()) - self.findList.resizeColumnToContents(1) - if self.__replaceMode: - self.findList.header().resizeSection(0, self.__section0Size + 30) - self.findList.header().setStretchLastSection(True) - self.__populating = False - - self.stopButton.setEnabled(False) - self.findButton.setEnabled(True) - self.findButton.setDefault(True) - - if breakSearch: - self.close() - - def on_findList_itemDoubleClicked(self, itm, column): - """ - Private slot to handle the double click on a file item. - - It emits the signal - sourceFile or designerFile depending on the file extension. - - @param itm the double clicked tree item (QTreeWidgetItem) - @param column column that was double clicked (integer) (ignored) - """ - if itm.parent(): - file = itm.parent().text(0) - line = itm.data(0, self.lineRole) - start = itm.data(0, self.startRole) - end = itm.data(0, self.endRole) - else: - file = itm.text(0) - line = 1 - start = 0 - end = 0 - - fn = os.path.join(self.project.ppath, file) if self.project else file - if fn.endswith('.ui'): - self.designerFile.emit(fn) - else: - self.sourceFile.emit(fn, line, "", start, end) - - def __getFileList(self, path, filterRe, excludeHiddenDirs=False, - excludeHiddenFiles=False): - """ - Private method to get a list of files to search. - - @param path the root directory to search in - @type str - @param filterRe regular expression defining the filter - criteria - @type regexp object - @param excludeHiddenDirs flag indicating to exclude hidden directories - @type bool - @param excludeHiddenFiles flag indicating to exclude hidden files - @type bool - @return list of files to be processed - @rtype list of str - """ - path = os.path.abspath(path) - files = [] - for dirname, dirs, filenames in os.walk(path): - files.extend([ - os.path.join(dirname, f) for f in filenames - if (not (excludeHiddenFiles and f.startswith(".")) and - re.match(filterRe, f)) - ]) - if excludeHiddenDirs: - for d in dirs[:]: - if d .startswith("."): - dirs.remove(d) - return files - - def setSearchDirectory(self, searchDir): - """ - Public slot to set the name of the directory to search in. - - @param searchDir name of the directory to search in (string) - """ - self.dirButton.setChecked(True) - self.dirPicker.setEditText(Utilities.toNativeSeparators(searchDir)) - - def setOpenFiles(self): - """ - Public slot to set the mode to search in open files. - """ - self.openFilesButton.setChecked(True) - - @pyqtSlot() - def on_replaceButton_clicked(self): - """ - Private slot to perform the requested replace actions. - """ - self.findProgress.setMaximum(self.findList.topLevelItemCount()) - self.findProgress.setValue(0) - - for index in range(self.findList.topLevelItemCount()): - itm = self.findList.topLevelItem(index) - if itm.checkState(0) in [Qt.CheckState.PartiallyChecked, - Qt.CheckState.Checked]: - file = itm.text(0) - origHash = itm.data(0, self.md5Role) - - self.findProgressLabel.setPath(file) - - if self.projectButton.isChecked(): - fn = os.path.join(self.project.ppath, file) - else: - fn = file - - # read the file and split it into textlines - try: - text, encoding, hashStr = ( - Utilities.readEncodedFileWithHash(fn) - ) - lines = text.splitlines(True) - except (UnicodeError, OSError) as err: - EricMessageBox.critical( - self, - self.tr("Replace in Files"), - self.tr( - """<p>Could not read the file <b>{0}</b>.""" - """ Skipping it.</p><p>Reason: {1}</p>""") - .format(fn, str(err)) - ) - self.findProgress.setValue(index) - continue - - # Check the original and the current hash. Skip the file, - # if hashes are different. - if origHash != hashStr: - EricMessageBox.critical( - self, - self.tr("Replace in Files"), - self.tr( - """<p>The current and the original hash of the""" - """ file <b>{0}</b> are different. Skipping it.""" - """</p><p>Hash 1: {1}</p><p>Hash 2: {2}</p>""") - .format(fn, origHash, hashStr) - ) - self.findProgress.setValue(index) - continue - - # replace the lines authorized by the user - for cindex in range(itm.childCount()): - citm = itm.child(cindex) - if citm.checkState(0) == Qt.CheckState.Checked: - line = citm.data(0, self.lineRole) - rline = citm.data(0, self.replaceRole) - lines[line - 1] = rline - - # write the file - txt = "".join(lines) - try: - Utilities.writeEncodedFile(fn, txt, encoding) - except (OSError, Utilities.CodingError, UnicodeError) as err: - EricMessageBox.critical( - self, - self.tr("Replace in Files"), - self.tr( - """<p>Could not save the file <b>{0}</b>.""" - """ Skipping it.</p><p>Reason: {1}</p>""") - .format(fn, str(err)) - ) - - self.findProgress.setValue(index + 1) - - self.findProgressLabel.setPath("") - - self.findList.clear() - self.replaceButton.setEnabled(False) - self.findButton.setEnabled(True) - self.findButton.setDefault(True) - - def __contextMenuRequested(self, pos): - """ - Private slot to handle the context menu request. - - @param pos position the context menu shall be shown (QPoint) - """ - menu = QMenu(self) - - menu.addAction(self.tr("Open"), self.__openFile) - menu.addAction(self.tr("Copy Path to Clipboard"), - self.__copyToClipboard) - - menu.exec(QCursor.pos()) - - def __openFile(self): - """ - Private slot to open the currently selected entry. - """ - itm = self.findList.selectedItems()[0] - self.on_findList_itemDoubleClicked(itm, 0) - - def __copyToClipboard(self): - """ - Private method to copy the path of an entry to the clipboard. - """ - itm = self.findList.selectedItems()[0] - fn = itm.parent().text(0) if itm.parent() else itm.text(0) - - cb = QApplication.clipboard() - cb.setText(fn)