Use regex search instead

This commit is contained in:
Veronica Berglyd Olsen
2024-03-24 14:52:32 +01:00
parent 9153707075
commit 026aeb3d82
2 changed files with 73 additions and 27 deletions
+34 -7
View File
@@ -34,7 +34,7 @@ from functools import partial
from zipfile import ZipFile, is_zipfile from zipfile import ZipFile, is_zipfile
from collections.abc import Iterable from collections.abc import Iterable
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import isHandle, minmax, simplified from novelwriter.common import isHandle, minmax, simplified
@@ -306,8 +306,13 @@ class DocDuplicator:
class DocSearch: class DocSearch:
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject, regEx: bool, doCase: bool, wordsOnly: bool) -> None:
self._project = project self._project = project
self._escape = not regEx
self._words = wordsOnly and not regEx
self._rxOpts = QRegularExpression.PatternOption.UseUnicodePropertiesOption
if not doCase:
self._rxOpts |= QRegularExpression.PatternOption.CaseInsensitiveOption
return return
def iterSearch(self, search: str) -> Iterable[tuple[NWItem, list[tuple[int, int, str]]]]: def iterSearch(self, search: str) -> Iterable[tuple[NWItem, list[tuple[int, int, str]]]]:
@@ -315,20 +320,42 @@ class DocSearch:
num = len(search) num = len(search)
cap = min(num+100, 100) cap = min(num+100, 100)
storage = self._project.storage storage = self._project.storage
regEx = QRegularExpression(self._buildPattern(search), self._rxOpts)
print(regEx.pattern())
for item in self._project.tree: for item in self._project.tree:
if item.isFileType(): if item.isFileType():
text = storage.getDocument(item.itemHandle).readDocument() or "" text = storage.getDocument(item.itemHandle).readDocument() or ""
prev = 0 rxItt = regEx.globalMatch(text)
results = [] results = []
count = 0 while rxItt.hasNext():
while (pos := text.find(search, prev)) >= 0 and count < 100: rxMatch = rxItt.next()
count += 1 pos = rxMatch.capturedStart()
num = rxMatch.capturedLength()
context = text[pos:pos+cap].partition("\n")[0] context = text[pos:pos+cap].partition("\n")[0]
results.append((pos, num, context)) results.append((pos, num, context))
prev = pos + num
yield item, results yield item, results
return return
##
# Internal Functions
##
def _buildPattern(self, search: str) -> str:
"""Build the search pattern string."""
if self._escape:
if CONFIG.verQtValue >= 0x050f00:
search = QRegularExpression.escape(search)
else:
# For older Qt versions, we escape manually
escaped = ""
for c in search:
if c.isalnum() or c == "_":
escaped += c
else:
escaped += f"\\{c}"
search = escaped
return f"\\b{search}\\b" if self._words else search
# END Class DocSearch # END Class DocSearch
+39 -20
View File
@@ -25,18 +25,16 @@ from __future__ import annotations
import logging import logging
from PyQt5.QtCore import pyqtSlot from PyQt5.QtCore import QSize, Qt, pyqtSlot
from PyQt5.QtGui import QPalette from PyQt5.QtGui import QPalette
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QHBoxLayout, QLabel, QLineEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QToolBar, QTreeWidget, QTreeWidgetItem,
QWidget QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.core.coretools import DocSearch from novelwriter.core.coretools import DocSearch
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -56,28 +54,46 @@ class GuiProjectSearch(QWidget):
self.viewLabel.setFont(SHARED.theme.guiFontB) self.viewLabel.setFont(SHARED.theme.guiFontB)
self.viewLabel.setContentsMargins(mPx, mPx, 0, mPx) self.viewLabel.setContentsMargins(mPx, mPx, 0, mPx)
# Options
self.searchOpt = QToolBar(self)
self.searchOpt.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.searchOpt.setIconSize(QSize(iPx, iPx))
self.searchOpt.setContentsMargins(0, 0, 0, 0)
self.toggleCase = self.searchOpt.addAction(self.tr("Case Sensitive"))
self.toggleCase.setCheckable(True)
self.toggleWord = self.searchOpt.addAction(self.tr("Whole Words Only"))
self.toggleWord.setCheckable(True)
self.toggleRegEx = self.searchOpt.addAction(self.tr("RegEx Mode"))
self.toggleRegEx.setCheckable(True)
# Controls # Controls
self.searchText = QLineEdit(self) self.searchText = QLineEdit(self)
self.searchText.setPlaceholderText(self.tr("Search text ...")) self.searchText.setPlaceholderText(self.tr("Search text ..."))
self.searchText.setClearButtonEnabled(True)
self.searchButton = NIconToolButton(self, iPx) self.searchAction = self.searchText.addAction(
self.searchButton.clicked.connect(self._processSearch) SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition
)
self.searchBar = QHBoxLayout() self.searchAction.triggered.connect(self._processSearch)
self.searchBar.addWidget(self.searchText)
self.searchBar.addWidget(self.searchButton)
# Search Result # Search Result
self.searchResult = QTreeWidget(self) self.searchResult = QTreeWidget(self)
self.searchResult.setHeaderHidden(True) self.searchResult.setHeaderHidden(True)
# Assemble # Assemble
self.headerBox = QHBoxLayout()
self.headerBox.addWidget(self.viewLabel, 1)
self.headerBox.addWidget(self.searchOpt, 0)
self.headerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.outerBox.addWidget(self.viewLabel, 0) self.outerBox.addLayout(self.headerBox, 0)
self.outerBox.addLayout(self.searchBar, 0) self.outerBox.addWidget(self.searchText, 0)
self.outerBox.addWidget(self.searchResult, 1) self.outerBox.addWidget(self.searchResult, 1)
self.outerBox.setContentsMargins(0, 0, 0, 0) self.outerBox.setContentsMargins(0, 0, 0, 0)
self.outerBox.setSpacing(0) self.outerBox.setSpacing(mPx)
self.setLayout(self.outerBox) self.setLayout(self.outerBox)
self.updateTheme() self.updateTheme()
@@ -96,10 +112,10 @@ class GuiProjectSearch(QWidget):
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base()) qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
self.setPalette(qPalette) self.setPalette(qPalette)
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON) self.searchAction.setIcon(SHARED.theme.getIcon("search"))
self.searchButton.setStyleSheet(buttonStyle) self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
self.searchButton.setIcon(SHARED.theme.getIcon("search")) self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
return return
@@ -112,7 +128,10 @@ class GuiProjectSearch(QWidget):
"""Perform a search.""" """Perform a search."""
self.searchResult.clear() self.searchResult.clear()
if text := self.searchText.text(): if text := self.searchText.text():
search = DocSearch(SHARED.project) search = DocSearch(
SHARED.project, self.toggleRegEx.isChecked(),
self.toggleCase.isChecked(), self.toggleWord.isChecked()
)
for item, results in search.iterSearch(text): for item, results in search.iterSearch(text):
self._appendResultSet(item, results) self._appendResultSet(item, results)
return return