From 9153707075997c9af21443f5088d6ef20370d41c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 23 Mar 2024 23:54:41 +0100 Subject: [PATCH] Add basic string search --- novelwriter/core/coretools.py | 28 +++++++++++++++++++++ novelwriter/gui/search.py | 47 ++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index a4e3e3aa..9e87e595 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -304,6 +304,34 @@ class DocDuplicator: # END Class DocDuplicator +class DocSearch: + + def __init__(self, project: NWProject) -> None: + self._project = project + return + + def iterSearch(self, search: str) -> Iterable[tuple[NWItem, list[tuple[int, int, str]]]]: + """Iteratively search through documents in the project.""" + num = len(search) + cap = min(num+100, 100) + storage = self._project.storage + for item in self._project.tree: + if item.isFileType(): + text = storage.getDocument(item.itemHandle).readDocument() or "" + prev = 0 + results = [] + count = 0 + while (pos := text.find(search, prev)) >= 0 and count < 100: + count += 1 + context = text[pos:pos+cap].partition("\n")[0] + results.append((pos, num, context)) + prev = pos + num + yield item, results + return + +# END Class DocSearch + + class ProjectBuilder: """A class to build a new project from a set of user-defined parameter provided by the New Project Wizard. diff --git a/novelwriter/gui/search.py b/novelwriter/gui/search.py index 70b84de8..72826e85 100644 --- a/novelwriter/gui/search.py +++ b/novelwriter/gui/search.py @@ -25,10 +25,16 @@ from __future__ import annotations import logging +from PyQt5.QtCore import pyqtSlot from PyQt5.QtGui import QPalette -from PyQt5.QtWidgets import QHBoxLayout, QLabel, QLineEdit, QTreeWidget, QVBoxLayout, QWidget +from PyQt5.QtWidgets import ( + QHBoxLayout, QLabel, QLineEdit, 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 @@ -51,13 +57,14 @@ class GuiProjectSearch(QWidget): self.viewLabel.setContentsMargins(mPx, mPx, 0, mPx) # Controls - self.searchBox = QLineEdit(self) - self.searchBox.setPlaceholderText(self.tr("Search text ...")) + 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.searchBox) + self.searchBar.addWidget(self.searchText) self.searchBar.addWidget(self.searchButton) # Search Result @@ -96,4 +103,36 @@ class GuiProjectSearch(QWidget): return + ## + # Private Slots + ## + + @pyqtSlot() + def _processSearch(self) -> None: + """Perform a search.""" + self.searchResult.clear() + if text := self.searchText.text(): + search = DocSearch(SHARED.project) + for item, results in search.iterSearch(text): + self._appendResultSet(item, results) + return + + ## + # Internal Functions + ## + + def _appendResultSet(self, item: NWItem, results: list[tuple[int, int, str]]) -> None: + """Populate the result tree.""" + if results: + tItem = QTreeWidgetItem() + tItem.setText(0, f"{item.itemName} ({len(results)})") + rItems = [] + for start, end, context in results: + rItem = QTreeWidgetItem() + rItem.setText(0, context) + rItems.append(rItem) + tItem.addChildren(rItems) + self.searchResult.addTopLevelItem(tItem) + return + # END Class GuiProjectSearch