Fix spell checking when 4 byte are in use

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