Use Python regex for document search tool

This commit is contained in:
Veronica Berglyd Olsen
2024-09-22 17:47:49 +02:00
parent 37c469dbc7
commit 9b08ebbbd5
3 changed files with 14 additions and 20 deletions
+12 -14
View File
@@ -27,6 +27,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations from __future__ import annotations
import logging import logging
import re
import shutil import shutil
from collections.abc import Iterable from collections.abc import Iterable
@@ -34,7 +35,7 @@ from functools import partial
from pathlib import Path from pathlib import Path
from zipfile import ZipFile, is_zipfile from zipfile import ZipFile, is_zipfile
from PyQt5.QtCore import QCoreApplication, QRegularExpression from PyQt5.QtCore import QCoreApplication
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import isHandle, minmax, simplified from novelwriter.common import isHandle, minmax, simplified
@@ -297,8 +298,8 @@ class DocDuplicator:
class DocSearch: class DocSearch:
def __init__(self) -> None: def __init__(self) -> None:
self._regEx = QRegularExpression() self._regEx = re.compile("")
self.setCaseSensitive(False) self._opts = re.UNICODE | re.IGNORECASE
self._words = False self._words = False
self._escape = True self._escape = True
return return
@@ -309,10 +310,9 @@ class DocSearch:
def setCaseSensitive(self, state: bool) -> None: def setCaseSensitive(self, state: bool) -> None:
"""Set the case sensitive search flag.""" """Set the case sensitive search flag."""
opts = QRegularExpression.PatternOption.UseUnicodePropertiesOption self._opts = re.UNICODE
if not state: if not state:
opts |= QRegularExpression.PatternOption.CaseInsensitiveOption self._opts |= re.IGNORECASE
self._regEx.setPatternOptions(opts)
return return
def setWholeWords(self, state: bool) -> None: def setWholeWords(self, state: bool) -> None:
@@ -329,8 +329,8 @@ class DocSearch:
self, project: NWProject, search: str self, project: NWProject, search: str
) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]: ) -> Iterable[tuple[NWItem, list[tuple[int, int, str]], bool]]:
"""Iteratively search through documents in a project.""" """Iteratively search through documents in a project."""
self._regEx.setPattern(self._buildPattern(search)) self._regEx = re.compile(self._buildPattern(search), self._opts)
logger.debug("Searching with pattern '%s'", self._regEx.pattern()) logger.debug("Searching with pattern '%s'", self._regEx.pattern)
storage = project.storage storage = project.storage
for item in project.tree: for item in project.tree:
if item.isFileType(): if item.isFileType():
@@ -340,14 +340,12 @@ class DocSearch:
def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]: def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]:
"""Search a piece of text for RegEx matches.""" """Search a piece of text for RegEx matches."""
rxItt = self._regEx.globalMatch(text)
count = 0 count = 0
capped = False capped = False
results = [] results = []
while rxItt.hasNext(): for match in re.finditer(self._regEx, text):
rxMatch = rxItt.next() pos = match.start(0)
pos = rxMatch.capturedStart() num = len(match.group(0))
num = rxMatch.capturedLength()
lim = text[:pos].rfind("\n") + 1 lim = text[:pos].rfind("\n") + 1
cut = text[lim:pos].rfind(" ") + lim + 1 cut = text[lim:pos].rfind(" ") + lim + 1
context = text[cut:cut+100].partition("\n")[0] context = text[cut:cut+100].partition("\n")[0]
@@ -366,7 +364,7 @@ class DocSearch:
def _buildPattern(self, search: str) -> str: def _buildPattern(self, search: str) -> str:
"""Build the search pattern string.""" """Build the search pattern string."""
if self._escape: if self._escape:
search = QRegularExpression.escape(search) search = re.escape(search)
if self._words: if self._words:
search = f"(?:^|\\b){search}(?:$|\\b)" search = f"(?:^|\\b){search}(?:$|\\b)"
return search return search
+1 -5
View File
@@ -23,7 +23,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from __future__ import annotations from __future__ import annotations
from PyQt5.QtCore import QRegularExpression, Qt from PyQt5.QtCore import Qt
from PyQt5.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat from PyQt5.QtGui import QColor, QFont, QPainter, QTextCharFormat, QTextCursor, QTextFormat
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QStyle from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QSizePolicy, QStyle
@@ -115,10 +115,6 @@ QtSizeMinimumExpanding = QSizePolicy.Policy.MinimumExpanding
QtScrollAlwaysOff = Qt.ScrollBarPolicy.ScrollBarAlwaysOff QtScrollAlwaysOff = Qt.ScrollBarPolicy.ScrollBarAlwaysOff
QtScrollAsNeeded = Qt.ScrollBarPolicy.ScrollBarAsNeeded QtScrollAsNeeded = Qt.ScrollBarPolicy.ScrollBarAsNeeded
# Other
QRegExUnicode = QRegularExpression.PatternOption.UseUnicodePropertiesOption
# Maps # Maps
FONT_WEIGHTS: dict[int, int] = { FONT_WEIGHTS: dict[int, int] = {
+1 -1
View File
@@ -421,7 +421,7 @@ def testCoreTools_DocSearch(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
# Patterns # Patterns
# ======== # ========
# Escape Using QRegularExpression # Escape
assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+" assert search._buildPattern("[A-Za-z0-9_]+") == r"\[A\-Za\-z0\-9_\]\+"
# Whole Words # Whole Words