diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 1dfee673..7af87ac9 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1202,13 +1202,13 @@ class GuiDocEditor(QPlainTextEdit): # Spell Checking if SHARED.project.data.spellCheck: - word, cPos, cLen, suggest = self._qDocument.spellErrorAtPos(pCursor.position()) - if word and cPos >= 0 and cLen > 0: + word, offset, suggest = self._qDocument.spellErrorAtPos(pCursor.position()) + if word and offset >= 0: logger.debug("Word '%s' is misspelled", word) block = pCursor.block() sCursor = self.textCursor() - sCursor.setPosition(block.position() + cPos) - sCursor.movePosition(QtMoveRight, QtKeepAnchor, cLen) + sCursor.setPosition(block.position() + offset) + sCursor.movePosition(QtMoveRight, QtKeepAnchor, len(word)) if suggest: ctxMenu.addSeparator() qtAddAction(ctxMenu, self.tr("Spelling Suggestion(s)")) diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index e1e172c5..24e976fd 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -323,24 +323,23 @@ class GuiDocHighlighter(QSyntaxHighlighter): if text.startswith("@"): # Keywords and commands self.setCurrentBlockState(BLOCK_META) index = SHARED.project.index - isValid, bits, pos = index.scanThis(text) + isValid, bits, loc = index.scanThis(text) isGood = index.checkThese(bits, self._tHandle) if isValid: for n, bit in enumerate(bits): - xPos = utf16Map[pos[n]] if utf16Map else pos[n] - xLen = utf16Map[pos[n] + len(bit)] - xPos if utf16Map else len(bit) + pos = utf16Map[loc[n]] if utf16Map else loc[n] + length = utf16Map[loc[n] + len(bit)] - pos if utf16Map else len(bit) if n == 0 and isGood[n]: - self.setFormat(xPos, xLen, self._hStyles["keyword"]) + self.setFormat(pos, length, self._hStyles["keyword"]) elif isGood[n] and not self._isInactive: a, b = index.parseValue(bit) - aLen = utf16Map[pos[n] + len(a)] - xPos if utf16Map else len(a) - self.setFormat(xPos, aLen, self._hStyles["tag"]) + aLen = utf16Map[loc[n] + len(a)] - pos if utf16Map else len(a) + self.setFormat(pos, aLen, self._hStyles["tag"]) if b: - blockLen = utf16Map[pos[n] + len(b)] - xPos if utf16Map else len(b) - bPos = xPos + xLen - blockLen - self.setFormat(bPos, blockLen, self._hStyles["optional"]) + bLen = utf16Map[loc[n] + len(b)] - pos if utf16Map else len(b) + self.setFormat(pos + length - bLen, bLen, self._hStyles["optional"]) elif not self._isInactive: - self.setFormat(xPos, xLen, self._hStyles["invalid"]) + self.setFormat(pos, length, self._hStyles["invalid"]) # We never want to run the spell checker on keyword/values, # so we force a return here @@ -386,19 +385,19 @@ class GuiDocHighlighter(QSyntaxHighlighter): if utf16Map: dot = utf16Map[dot] pos = utf16Map[pos] - cLen = blockLen - pos + length = blockLen - pos if style == nwComment.PLAIN: - self.setFormat(0, cLen, self._hStyles["hidden"]) + self.setFormat(0, length, self._hStyles["hidden"]) elif style == nwComment.IGNORE: - self.setFormat(0, cLen, self._hStyles["strike"]) + self.setFormat(0, length, self._hStyles["strike"]) return # No more processing for these elif mod: self.setFormat(0, dot, self._hStyles["modifier"]) self.setFormat(dot, pos - dot, self._hStyles["value"]) - self.setFormat(pos, cLen, self._hStyles["note"]) + self.setFormat(pos, length, self._hStyles["note"]) else: self.setFormat(0, pos, self._hStyles["modifier"]) - self.setFormat(pos, cLen, self._hStyles["note"]) + self.setFormat(pos, length, self._hStyles["note"]) elif text.startswith("["): # Special Command self.setCurrentBlockState(BLOCK_TEXT) @@ -409,12 +408,12 @@ class GuiDocHighlighter(QSyntaxHighlighter): self.setFormat(0, blockLen, self._hStyles["code"]) return elif check.startswith("[vspace:") and check.endswith("]"): - tLen = len(check) - tVal = checkInt(check[8:-1], 0) - cVal = "value" if tVal > 0 else "invalid" + length = len(check) + value = checkInt(check[8:-1], 0) + style = "value" if value > 0 else "invalid" self.setFormat(0, 8, self._hStyles["code"]) - self.setFormat(8, tLen-9, self._hStyles[cVal]) - self.setFormat(tLen-1, tLen, self._hStyles["code"]) + self.setFormat(8, length-9, self._hStyles[style]) + self.setFormat(length-1, length, self._hStyles["code"]) return else: # Text Paragraph @@ -462,19 +461,11 @@ class GuiDocHighlighter(QSyntaxHighlighter): data.processText(text, offset) if self._spellCheck: - if utf16Map: - for pos, end in data.spellCheck(): - for x in range(pos, end): - m = utf16Map[x] - cFmt = self.format(m) - cFmt.merge(self._spellErr) - self.setFormat(m, utf16Map[x+1] - m, cFmt) - else: - for pos, end in data.spellCheck(): - for x in range(pos, end): - cFmt = self.format(x) - cFmt.merge(self._spellErr) - self.setFormat(x, 1, cFmt) + for pos, end, _ in data.spellCheck(utf16Map): + for x in range(pos, end): + cFmt = self.format(x) + cFmt.merge(self._spellErr) + self.setFormat(x, 1, cFmt) return @@ -528,7 +519,7 @@ class TextBlockData(QTextBlockUserData): self._text = "" self._offset = 0 self._metaData: list[tuple[int, int, str, str]] = [] - self._spellErrors: list[tuple[int, int]] = [] + self._spellErrors: list[tuple[int, int, str]] = [] return @property @@ -537,7 +528,7 @@ class TextBlockData(QTextBlockUserData): return self._metaData @property - def spellErrors(self) -> list[tuple[int, int]]: + def spellErrors(self) -> list[tuple[int, int, str]]: """Return spell error data from last check.""" return self._spellErrors @@ -565,13 +556,21 @@ class TextBlockData(QTextBlockUserData): return - def spellCheck(self) -> list[tuple[int, int]]: + def spellCheck(self, utf16Map: list[int] | None) -> list[tuple[int, int, str]]: """Run the spell checker and cache the result, and return the list of spell check errors. """ spell = SHARED.spelling - self._spellErrors = [ - (r.start(0), r.end(0)) for r in RX_WORDS.finditer(self._text, self._offset) - if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w)) - ] + if utf16Map: + self._spellErrors = [ + (utf16Map[r.start(0)], utf16Map[r.end(0)], w) + for r in RX_WORDS.finditer(self._text, self._offset) + if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w)) + ] + else: + self._spellErrors = [ + (r.start(0), r.end(0), w) + for r in RX_WORDS.finditer(self._text, self._offset) + if (w := r.group(0)) and not (w.isnumeric() or w.isupper() or spell.checkWord(w)) + ] return self._spellErrors diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py index d42ffaf1..04286e9f 100644 --- a/novelwriter/gui/editordocument.py +++ b/novelwriter/gui/editordocument.py @@ -113,7 +113,7 @@ class GuiTextDocument(QTextDocument): return cData, cType return "", "" - def spellErrorAtPos(self, pos: int) -> tuple[str, int, int, list[str]]: + def spellErrorAtPos(self, pos: int) -> tuple[str, int, list[str]]: """Check if there is a misspelled word at a given position in the document, and if so, return it. """ @@ -122,15 +122,11 @@ class GuiTextDocument(QTextDocument): block = cursor.block() data = block.userData() if block.isValid() and isinstance(data, TextBlockData): - text = block.text() - check = pos - block.position() - if check >= 0: - for cPos, cEnd in data.spellErrors: - cLen = cEnd - cPos - if cPos <= check <= cEnd: - word = text[cPos:cEnd] - return word, cPos, cLen, SHARED.spelling.suggestWords(word) - return "", -1, -1, [] + if (check := pos - block.position()) >= 0: + for start, end, word in data.spellErrors: + if start <= check <= end: + return word, start, SHARED.spelling.suggestWords(word) + return "", -1, [] def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Iterable[QTextBlock]: """Iterate over all text blocks of a given type.""" diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 52cd8826..699bbaaa 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -520,10 +520,10 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText, data = cursor.block().userData() assert cursor.block().text().startswith("Lorem") assert isinstance(data, TextBlockData) - data._spellErrors = [(0, 5)] + data._spellErrors = [(0, 5, "Lorem")] # No known position - assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, -1, []) + assert docEditor._qDocument.spellErrorAtPos(-1) == ("", -1, []) # With Suggestion with monkeypatch.context() as mp: @@ -540,6 +540,9 @@ def testGuiEditor_SpellChecking(qtbot, monkeypatch, nwGUI, projPath, ipsumText, ctxMenu.setObjectName("") ctxMenu.deleteLater() + # Update Entry + data._spellErrors = [(0, 5, "Lorax")] + # Without Suggestion with monkeypatch.context() as mp: mp.setattr(SHARED.spelling, "suggestWords", lambda *a: [])