From 5ed03e29f37069efbdd25f2ef5955a614ac5d086 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:20:09 +0200 Subject: [PATCH 1/2] Change regexes to use the pattern object directly --- novelwriter/core/coretools.py | 2 +- novelwriter/core/tokenizer.py | 8 ++++---- novelwriter/gui/dochighlight.py | 15 +++++++-------- tests/test_text/test_text_patterns.py | 2 +- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 5b4dc6d7..72f6ca8e 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -343,7 +343,7 @@ class DocSearch: count = 0 capped = False results = [] - for match in re.finditer(self._regEx, text): + for match in self._regEx.finditer(text): pos = match.start(0) num = len(match.group(0)) lim = text[:pos].rfind("\n") + 1 diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index 4c90127a..8dc4f096 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -1109,14 +1109,14 @@ class Tokenizer(ABC): # Match Markdown for regEx, fmts in self._rxMarkdown: - for match in re.finditer(regEx, text): + for match in regEx.finditer(text): temp.extend( (match.start(n), match.end(n), fmt, "") for n, fmt in enumerate(fmts) if fmt > 0 ) # Match Shortcodes - for match in re.finditer(REGEX_PATTERNS.shortcodePlain, text): + for match in REGEX_PATTERNS.shortcodePlain.finditer(text): temp.append(( match.start(1), match.end(1), self._shortCodeFmt.get(match.group(1).lower(), 0), @@ -1125,7 +1125,7 @@ class Tokenizer(ABC): # Match Shortcode w/Values tHandle = self._handle or "" - for match in re.finditer(REGEX_PATTERNS.shortcodeValue, text): + for match in REGEX_PATTERNS.shortcodeValue.finditer(text): kind = self._shortCodeVals.get(match.group(1).lower(), 0) temp.append(( match.start(0), match.end(0), @@ -1136,7 +1136,7 @@ class Tokenizer(ABC): # Match Dialogue if self._rxDialogue and hDialog: for regEx, fmtB, fmtE in self._rxDialogue: - for match in re.finditer(regEx, text): + for match in regEx.finditer(text): temp.append((match.start(0), 0, fmtB, "")) temp.append((match.end(0), 0, fmtE, "")) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 1af885e2..a19e0945 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -483,20 +483,19 @@ class TextBlockData(QTextBlockUserData): """ if "[" in text: # Strip shortcodes - for rX in [RX_FMT_SC, RX_FMT_SV]: - for match in re.finditer(rX, text[offset:]): - iS = match.start(0) + offset - iE = match.end(0) + offset - if iS >= 0 and iE >= 0: - text = text[:iS] + " "*(iE - iS) + text[iE:] + for regEx in [RX_FMT_SC, RX_FMT_SV]: + for match in regEx.finditer(text, offset): + if (s := match.start(0)) >= 0 and (e := match.end(0)) >= 0: + pad = " "*(e - s) + text = f"{text[:s]}{pad}{text[e:]}" self._spellErrors = [] checker = SHARED.spelling - for match in re.finditer(RX_WORDS, text[offset:].replace("_", " ")): + for match in RX_WORDS.finditer(text.replace("_", " ")): if ( (word := match.group(0)) and not (word.isnumeric() or word.isupper() or checker.checkWord(word)) ): - self._spellErrors.append((match.start(0) + offset, match.end(0) + offset)) + self._spellErrors.append((match.start(0), match.end(0))) return self._spellErrors diff --git a/tests/test_text/test_text_patterns.py b/tests/test_text/test_text_patterns.py index 7f1def90..ae021169 100644 --- a/tests/test_text/test_text_patterns.py +++ b/tests/test_text/test_text_patterns.py @@ -32,7 +32,7 @@ from novelwriter.text.patterns import REGEX_PATTERNS def allMatches(regEx: re.Pattern, text: str) -> list[list[str]]: """Get all matches for a regex.""" result = [] - for match in re.finditer(regEx, text): + for match in regEx.finditer(text): result.append([ (match.group(n), match.start(n), match.end(n)) for n in range((match.lastindex or 0) + 1) From a6494b4226300c4e4dcd73edbf07a91c685c2f53 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 28 Sep 2024 11:21:04 +0200 Subject: [PATCH 2/2] Allow words to be ignored by the spell checker (session only) --- novelwriter/core/spellcheck.py | 23 +++++------- novelwriter/gui/doceditor.py | 50 ++++++++++++------------- tests/test_core/test_core_spellcheck.py | 12 ++++-- tests/test_gui/test_gui_doceditor.py | 6 ++- 4 files changed, 48 insertions(+), 43 deletions(-) diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py index 587895c0..6d4facca 100644 --- a/novelwriter/core/spellcheck.py +++ b/novelwriter/core/spellcheck.py @@ -125,21 +125,16 @@ class NWSpellEnchant: except Exception: return [] - def addWord(self, word: str) -> bool: + def addWord(self, word: str, save: bool = True) -> None: """Add a word to the project dictionary.""" - word = word.strip() - if not word: - return False - try: - self._enchant.add_to_session(word) - except Exception: - return False - - added = self._userDict.add(word) - if added: - self._userDict.save() - - return added + if word := word.strip(): + try: + self._enchant.add_to_session(word) + except Exception: + return + if save and self._userDict.add(word): + self._userDict.save() + return def listDictionaries(self) -> list[tuple[str, str]]: """List available dictionaries.""" diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index e4af4f7b..56b42ec7 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1164,8 +1164,10 @@ class GuiDocEditor(QPlainTextEdit): ctxMenu.addAction(f"{nwUnicode.U_ENDASH} {trNone}") ctxMenu.addSeparator() + action = ctxMenu.addAction(self.tr("Ignore Word")) + action.triggered.connect(lambda: self._addWord(word, block, False)) action = ctxMenu.addAction(self.tr("Add Word to Dictionary")) - action.triggered.connect(lambda: self._addWord(word, block)) + action.triggered.connect(lambda: self._addWord(word, block, True)) # Execute the context menu ctxMenu.exec(self.viewport().mapToGlobal(pos)) @@ -1173,30 +1175,6 @@ class GuiDocEditor(QPlainTextEdit): return - @pyqtSlot("QTextCursor", str) - def _correctWord(self, cursor: QTextCursor, word: str) -> None: - """Slot for the spell check context menu triggering the - replacement of a word with the word from the dictionary. - """ - pos = cursor.selectionStart() - cursor.beginEditBlock() - cursor.removeSelectedText() - cursor.insertText(word) - cursor.endEditBlock() - cursor.setPosition(pos) - self.setTextCursor(cursor) - return - - @pyqtSlot(str, "QTextBlock") - def _addWord(self, word: str, block: QTextBlock) -> None: - """Slot for the spell check context menu triggered when the user - wants to add a word to the project dictionary. - """ - logger.debug("Added '%s' to project dictionary", word) - SHARED.spelling.addWord(word) - self._qDocument.syntaxHighlighter.rehighlightBlock(block) - return - @pyqtSlot() def _runDocumentTasks(self) -> None: """Run timer document tasks.""" @@ -1875,6 +1853,28 @@ class GuiDocEditor(QPlainTextEdit): # Internal Functions ## + def _correctWord(self, cursor: QTextCursor, word: str) -> None: + """Slot for the spell check context menu triggering the + replacement of a word with the word from the dictionary. + """ + pos = cursor.selectionStart() + cursor.beginEditBlock() + cursor.removeSelectedText() + cursor.insertText(word) + cursor.endEditBlock() + cursor.setPosition(pos) + self.setTextCursor(cursor) + return + + def _addWord(self, word: str, block: QTextBlock, save: bool) -> None: + """Slot for the spell check context menu triggered when the user + wants to add a word to the project dictionary. + """ + logger.debug("Added '%s' to project dictionary, %s", word, "saved" if save else "unsaved") + SHARED.spelling.addWord(word, save=save) + self._qDocument.syntaxHighlighter.rehighlightBlock(block) + return + def _processTag(self, cursor: QTextCursor | None = None, follow: bool = True, create: bool = False) -> nwTrinary: """Activated by Ctrl+Enter. Checks that we're in a block diff --git a/tests/test_core/test_core_spellcheck.py b/tests/test_core/test_core_spellcheck.py index b3ea1dd1..5b90063a 100644 --- a/tests/test_core/test_core_spellcheck.py +++ b/tests/test_core/test_core_spellcheck.py @@ -158,15 +158,21 @@ def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath): assert isinstance(spChk._enchant, FakeEnchant) assert spChk.checkWord("word") is True assert spChk.suggestWords("word") == [] - assert spChk.addWord("word") is True + spChk.addWord("word") + assert "word" in spChk._userDict # Set the dict to None, and check enchant error handling spChk = NWSpellEnchant(project) spChk._enchant = None # type: ignore assert spChk.checkWord("word") is True assert spChk.suggestWords("word") == [] - assert spChk.addWord("word") is False - assert spChk.addWord("\n\t ") is False + + spChk.addWord("word") + assert "word" not in spChk._userDict + + spChk.addWord("\n\t ") + assert "\n\t " not in spChk._userDict + assert spChk.describeDict() == ("", "") # Load the proper enchant package (twice) diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index eb40f824..7fef876f 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -453,9 +453,13 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText, ctxMenu = getMenuForPos(docEditor, 16) assert ctxMenu is not None actions = [x.text() for x in ctxMenu.actions() if x.text()] + assert "Ignore Word" in actions assert "Add Word to Dictionary" in actions + assert "Lorax" not in SHARED.spelling._userDict - ctxMenu.actions()[7].trigger() + ctxMenu.actions()[7].trigger() # Ignore + assert "Lorax" not in SHARED.spelling._userDict + ctxMenu.actions()[8].trigger() # Add assert "Lorax" in SHARED.spelling._userDict ctxMenu.setObjectName("") ctxMenu.deleteLater()