Add search refresh when document changes (#1782)
This commit is contained in:
@@ -337,32 +337,32 @@ class DocSearch:
|
|||||||
"""Iteratively search through documents in a project."""
|
"""Iteratively search through documents in a project."""
|
||||||
self._regEx.setPattern(self._buildPattern(search))
|
self._regEx.setPattern(self._buildPattern(search))
|
||||||
logger.debug("Searching with pattern '%s'", self._regEx.pattern())
|
logger.debug("Searching with pattern '%s'", self._regEx.pattern())
|
||||||
|
|
||||||
num = len(search)
|
|
||||||
storage = project.storage
|
storage = project.storage
|
||||||
for item in project.tree:
|
for item in project.tree:
|
||||||
if item.isFileType():
|
if item.isFileType():
|
||||||
text = storage.getDocumentText(item.itemHandle)
|
results, capped = self.searchText(storage.getDocumentText(item.itemHandle))
|
||||||
rxItt = self._regEx.globalMatch(text)
|
|
||||||
count = 0
|
|
||||||
capped = False
|
|
||||||
results = []
|
|
||||||
while rxItt.hasNext():
|
|
||||||
rxMatch = rxItt.next()
|
|
||||||
pos = rxMatch.capturedStart()
|
|
||||||
num = rxMatch.capturedLength()
|
|
||||||
context = text[pos:pos+100].partition("\n")[0]
|
|
||||||
if context:
|
|
||||||
results.append((pos, num, context))
|
|
||||||
count += 1
|
|
||||||
if count >= nwConst.MAX_SEARCH_RESULT:
|
|
||||||
capped = True
|
|
||||||
break
|
|
||||||
|
|
||||||
yield item, results, capped
|
yield item, results, capped
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]:
|
||||||
|
"""Search a piece of text for RegEx matches."""
|
||||||
|
rxItt = self._regEx.globalMatch(text)
|
||||||
|
count = 0
|
||||||
|
capped = False
|
||||||
|
results = []
|
||||||
|
while rxItt.hasNext():
|
||||||
|
rxMatch = rxItt.next()
|
||||||
|
pos = rxMatch.capturedStart()
|
||||||
|
num = rxMatch.capturedLength()
|
||||||
|
context = text[pos:pos+100].partition("\n")[0]
|
||||||
|
if context:
|
||||||
|
results.append((pos, num, context))
|
||||||
|
count += 1
|
||||||
|
if count >= nwConst.MAX_SEARCH_RESULT:
|
||||||
|
capped = True
|
||||||
|
break
|
||||||
|
return results, capped
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
# Custom Signals
|
# Custom Signals
|
||||||
statusMessage = pyqtSignal(str)
|
statusMessage = pyqtSignal(str)
|
||||||
docCountsChanged = pyqtSignal(str, int, int, int)
|
docCountsChanged = pyqtSignal(str, int, int, int)
|
||||||
|
docTextChanged = pyqtSignal(str, float)
|
||||||
editedStatusChanged = pyqtSignal(bool)
|
editedStatusChanged = pyqtSignal(bool)
|
||||||
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
loadDocumentTagRequest = pyqtSignal(str, Enum)
|
||||||
novelStructureChanged = pyqtSignal()
|
novelStructureChanged = pyqtSignal()
|
||||||
@@ -1193,18 +1194,19 @@ class GuiDocEditor(QPlainTextEdit):
|
|||||||
if self._docHandle is None:
|
if self._docHandle is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.wCounterDoc.isRunning():
|
|
||||||
logger.debug("Word counter is busy")
|
|
||||||
return
|
|
||||||
|
|
||||||
if time() - self._lastEdit < 25.0:
|
if time() - self._lastEdit < 25.0:
|
||||||
logger.debug("Running word counter")
|
logger.debug("Running document tasks")
|
||||||
SHARED.runInThreadPool(self.wCounterDoc)
|
if not self.wCounterDoc.isRunning():
|
||||||
|
SHARED.runInThreadPool(self.wCounterDoc)
|
||||||
|
|
||||||
self.docHeader.setOutline({
|
self.docHeader.setOutline({
|
||||||
block.blockNumber(): block.text()
|
block.blockNumber(): block.text()
|
||||||
for block in self._qDocument.iterBlockByType(BLOCK_TITLE, maxCount=30)
|
for block in self._qDocument.iterBlockByType(BLOCK_TITLE, maxCount=30)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if self._docChanged:
|
||||||
|
self.docTextChanged.emit(self._docHandle, self._lastEdit)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@pyqtSlot(int, int, int)
|
@pyqtSlot(int, int, int)
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ class GuiProjectSearch(QWidget):
|
|||||||
self._time = time()
|
self._time = time()
|
||||||
self._search = DocSearch()
|
self._search = DocSearch()
|
||||||
self._blocked = False
|
self._blocked = False
|
||||||
|
self._map: dict[str, tuple[int, float]] = {}
|
||||||
|
|
||||||
# Header
|
# Header
|
||||||
self.viewLabel = QLabel(self.tr("Project Search"))
|
self.viewLabel = QLabel(self.tr("Project Search"))
|
||||||
@@ -197,6 +198,7 @@ class GuiProjectSearch(QWidget):
|
|||||||
|
|
||||||
def closeProjectTasks(self) -> None:
|
def closeProjectTasks(self) -> None:
|
||||||
"""Run close project tasks."""
|
"""Run close project tasks."""
|
||||||
|
self._map = {}
|
||||||
self.searchText.clear()
|
self.searchText.clear()
|
||||||
self.searchResult.clear()
|
self.searchResult.clear()
|
||||||
return
|
return
|
||||||
@@ -228,6 +230,20 @@ class GuiProjectSearch(QWidget):
|
|||||||
super().keyPressEvent(event)
|
super().keyPressEvent(event)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Public Slots
|
||||||
|
##
|
||||||
|
|
||||||
|
@pyqtSlot(str, float)
|
||||||
|
def textChanged(self, tHandle: str, timeStamp: float) -> None:
|
||||||
|
"""Update search result for a specific document."""
|
||||||
|
if (entry := self._map.get(tHandle)) and timeStamp > entry[1]:
|
||||||
|
start = time()
|
||||||
|
results, capped = self._search.searchText(SHARED.mainGui.docEditor.getText())
|
||||||
|
self._displayResultSet(SHARED.project.tree[tHandle], results, capped)
|
||||||
|
logger.debug("Updated search for '%s' in %.3f ms", tHandle, 1000*(time() - start))
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Private Slots
|
# Private Slots
|
||||||
##
|
##
|
||||||
@@ -238,14 +254,16 @@ class GuiProjectSearch(QWidget):
|
|||||||
if not self._blocked:
|
if not self._blocked:
|
||||||
qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
|
||||||
start = time()
|
start = time()
|
||||||
|
SHARED.mainGui.saveDocument()
|
||||||
self._blocked = True
|
self._blocked = True
|
||||||
|
self._map = {}
|
||||||
self.searchResult.clear()
|
self.searchResult.clear()
|
||||||
if text := self.searchText.text():
|
if text := self.searchText.text():
|
||||||
self._search.setUserRegEx(self.toggleRegEx.isChecked())
|
self._search.setUserRegEx(self.toggleRegEx.isChecked())
|
||||||
self._search.setCaseSensitive(self.toggleCase.isChecked())
|
self._search.setCaseSensitive(self.toggleCase.isChecked())
|
||||||
self._search.setWholeWords(self.toggleWord.isChecked())
|
self._search.setWholeWords(self.toggleWord.isChecked())
|
||||||
for item, results, capped in self._search.iterSearch(SHARED.project, text):
|
for item, results, capped in self._search.iterSearch(SHARED.project, text):
|
||||||
self._appendResultSet(item, results, capped)
|
self._displayResultSet(item, results, capped)
|
||||||
logger.debug("Search took %.3f ms", 1000*(time() - start))
|
logger.debug("Search took %.3f ms", 1000*(time() - start))
|
||||||
self._time = time()
|
self._time = time()
|
||||||
qApp.restoreOverrideCursor()
|
qApp.restoreOverrideCursor()
|
||||||
@@ -293,11 +311,11 @@ class GuiProjectSearch(QWidget):
|
|||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
|
|
||||||
def _appendResultSet(
|
def _displayResultSet(
|
||||||
self, nwItem: NWItem, results: list[tuple[int, int, str]], capped: bool
|
self, nwItem: NWItem | None, results: list[tuple[int, int, str]], capped: bool
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Populate the result tree."""
|
"""Populate the result tree."""
|
||||||
if results:
|
if results and nwItem:
|
||||||
tHandle = nwItem.itemHandle
|
tHandle = nwItem.itemHandle
|
||||||
docIcon = SHARED.theme.getItemIcon(
|
docIcon = SHARED.theme.getItemIcon(
|
||||||
nwItem.itemType, nwItem.itemClass,
|
nwItem.itemType, nwItem.itemClass,
|
||||||
@@ -312,7 +330,11 @@ class GuiProjectSearch(QWidget):
|
|||||||
tItem.setText(self.C_COUNT, f"({len(results):n}{ext})")
|
tItem.setText(self.C_COUNT, f"({len(results):n}{ext})")
|
||||||
tItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
|
tItem.setTextAlignment(self.C_COUNT, Qt.AlignmentFlag.AlignRight)
|
||||||
tItem.setForeground(self.C_COUNT, self.palette().highlight())
|
tItem.setForeground(self.C_COUNT, self.palette().highlight())
|
||||||
self.searchResult.addTopLevelItem(tItem)
|
|
||||||
|
index = self._map.get(tHandle, (self.searchResult.topLevelItemCount(), 0.0))[0]
|
||||||
|
self.searchResult.takeTopLevelItem(index)
|
||||||
|
self.searchResult.insertTopLevelItem(index, tItem)
|
||||||
|
self._map[tHandle] = (index, time())
|
||||||
|
|
||||||
rItems = []
|
rItems = []
|
||||||
for start, length, context in results:
|
for start, length, context in results:
|
||||||
|
|||||||
@@ -262,6 +262,7 @@ class GuiMain(QMainWindow):
|
|||||||
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
|
self.docEditor.requestProjectItemSelected.connect(self.projView.setSelectedHandle)
|
||||||
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
|
self.docEditor.requestProjectItemRenamed.connect(self.projView.renameTreeItem)
|
||||||
self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
|
self.docEditor.requestNewNoteCreation.connect(self.projView.createNewNote)
|
||||||
|
self.docEditor.docTextChanged.connect(self.projSearch.textChanged)
|
||||||
|
|
||||||
self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
|
self.docViewer.documentLoaded.connect(self.docViewerPanel.updateHandle)
|
||||||
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
|
self.docViewer.loadDocumentTagRequest.connect(self._followTag)
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from time import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt
|
||||||
@@ -58,6 +60,7 @@ def testGuiDocSearch_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
|||||||
assert result == (handle, 3, 5)
|
assert result == (handle, 3, 5)
|
||||||
|
|
||||||
# Move down
|
# Move down
|
||||||
|
search.searchText.setFocus()
|
||||||
qtbot.keyClick(search, Qt.Key.Key_Down)
|
qtbot.keyClick(search, Qt.Key.Key_Down)
|
||||||
assert firstDoc.isSelected() is True
|
assert firstDoc.isSelected() is True
|
||||||
|
|
||||||
@@ -119,6 +122,11 @@ def testGuiDocSearch_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
|
|||||||
assert search.searchResult.topLevelItemCount() == 10
|
assert search.searchResult.topLevelItemCount() == 10
|
||||||
assert totalCount() == 34
|
assert totalCount() == 34
|
||||||
|
|
||||||
|
# Re-run search should not change the result
|
||||||
|
search.textChanged(handle, time() + 1000.0)
|
||||||
|
assert search.searchResult.topLevelItemCount() == 10
|
||||||
|
assert totalCount() == 34
|
||||||
|
|
||||||
# qtbot.stop()
|
# qtbot.stop()
|
||||||
nwGUI.closeProject()
|
nwGUI.closeProject()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user