Improve spell checking (#2030)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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, ""))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user