Use regex search instead
This commit is contained in:
@@ -34,7 +34,7 @@ from functools import partial
|
||||
from zipfile import ZipFile, is_zipfile
|
||||
from collections.abc import Iterable
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
from PyQt5.QtCore import QCoreApplication, QRegularExpression
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.common import isHandle, minmax, simplified
|
||||
@@ -306,8 +306,13 @@ class DocDuplicator:
|
||||
|
||||
class DocSearch:
|
||||
|
||||
def __init__(self, project: NWProject) -> None:
|
||||
def __init__(self, project: NWProject, regEx: bool, doCase: bool, wordsOnly: bool) -> None:
|
||||
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
|
||||
|
||||
def iterSearch(self, search: str) -> Iterable[tuple[NWItem, list[tuple[int, int, str]]]]:
|
||||
@@ -315,20 +320,42 @@ class DocSearch:
|
||||
num = len(search)
|
||||
cap = min(num+100, 100)
|
||||
storage = self._project.storage
|
||||
regEx = QRegularExpression(self._buildPattern(search), self._rxOpts)
|
||||
print(regEx.pattern())
|
||||
for item in self._project.tree:
|
||||
if item.isFileType():
|
||||
text = storage.getDocument(item.itemHandle).readDocument() or ""
|
||||
prev = 0
|
||||
rxItt = regEx.globalMatch(text)
|
||||
results = []
|
||||
count = 0
|
||||
while (pos := text.find(search, prev)) >= 0 and count < 100:
|
||||
count += 1
|
||||
while rxItt.hasNext():
|
||||
rxMatch = rxItt.next()
|
||||
pos = rxMatch.capturedStart()
|
||||
num = rxMatch.capturedLength()
|
||||
context = text[pos:pos+cap].partition("\n")[0]
|
||||
results.append((pos, num, context))
|
||||
prev = pos + num
|
||||
yield item, results
|
||||
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
|
||||
|
||||
|
||||
|
||||
+39
-20
@@ -25,18 +25,16 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot
|
||||
from PyQt5.QtCore import QSize, Qt, pyqtSlot
|
||||
from PyQt5.QtGui import QPalette
|
||||
from PyQt5.QtWidgets import (
|
||||
QHBoxLayout, QLabel, QLineEdit, QTreeWidget, QTreeWidgetItem, QVBoxLayout,
|
||||
QWidget
|
||||
QHBoxLayout, QLabel, QLineEdit, QToolBar, QTreeWidget, QTreeWidgetItem,
|
||||
QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
from novelwriter.core.coretools import DocSearch
|
||||
from novelwriter.core.item import NWItem
|
||||
from novelwriter.extensions.modified import NIconToolButton
|
||||
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -56,28 +54,46 @@ class GuiProjectSearch(QWidget):
|
||||
self.viewLabel.setFont(SHARED.theme.guiFontB)
|
||||
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
|
||||
self.searchText = QLineEdit(self)
|
||||
self.searchText.setPlaceholderText(self.tr("Search text ..."))
|
||||
|
||||
self.searchButton = NIconToolButton(self, iPx)
|
||||
self.searchButton.clicked.connect(self._processSearch)
|
||||
|
||||
self.searchBar = QHBoxLayout()
|
||||
self.searchBar.addWidget(self.searchText)
|
||||
self.searchBar.addWidget(self.searchButton)
|
||||
self.searchText.setClearButtonEnabled(True)
|
||||
self.searchAction = self.searchText.addAction(
|
||||
SHARED.theme.getIcon("search"), QLineEdit.ActionPosition.TrailingPosition
|
||||
)
|
||||
self.searchAction.triggered.connect(self._processSearch)
|
||||
|
||||
# Search Result
|
||||
self.searchResult = QTreeWidget(self)
|
||||
self.searchResult.setHeaderHidden(True)
|
||||
|
||||
# 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.addWidget(self.viewLabel, 0)
|
||||
self.outerBox.addLayout(self.searchBar, 0)
|
||||
self.outerBox.addLayout(self.headerBox, 0)
|
||||
self.outerBox.addWidget(self.searchText, 0)
|
||||
self.outerBox.addWidget(self.searchResult, 1)
|
||||
self.outerBox.setContentsMargins(0, 0, 0, 0)
|
||||
self.outerBox.setSpacing(0)
|
||||
self.outerBox.setSpacing(mPx)
|
||||
|
||||
self.setLayout(self.outerBox)
|
||||
self.updateTheme()
|
||||
@@ -96,10 +112,10 @@ class GuiProjectSearch(QWidget):
|
||||
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
|
||||
self.setPalette(qPalette)
|
||||
|
||||
buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
|
||||
self.searchButton.setStyleSheet(buttonStyle)
|
||||
|
||||
self.searchButton.setIcon(SHARED.theme.getIcon("search"))
|
||||
self.searchAction.setIcon(SHARED.theme.getIcon("search"))
|
||||
self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
|
||||
self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
|
||||
self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
|
||||
|
||||
return
|
||||
|
||||
@@ -112,7 +128,10 @@ class GuiProjectSearch(QWidget):
|
||||
"""Perform a search."""
|
||||
self.searchResult.clear()
|
||||
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):
|
||||
self._appendResultSet(item, results)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user