Skip to content

Commit

Permalink
Improve spell checking (#2030)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo authored Sep 28, 2024
2 parents 5c07148 + a6494b4 commit 7cc48ae
Show file tree
Hide file tree
Showing 8 changed files with 61 additions and 57 deletions.
2 changes: 1 addition & 1 deletion novelwriter/core/coretools.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ def searchText(self, text: str) -> tuple[list[tuple[int, int, str]], bool]:
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
Expand Down
23 changes: 9 additions & 14 deletions novelwriter/core/spellcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,21 +125,16 @@ def suggestWords(self, word: str) -> list[str]:
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."""
Expand Down
8 changes: 4 additions & 4 deletions novelwriter/core/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1109,14 +1109,14 @@ def _extractFormats(

# 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),
Expand All @@ -1125,7 +1125,7 @@ def _extractFormats(

# 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),
Expand All @@ -1136,7 +1136,7 @@ def _extractFormats(
# 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, ""))

Expand Down
50 changes: 25 additions & 25 deletions novelwriter/gui/doceditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1164,39 +1164,17 @@ def _openContextMenu(self, pos: QPoint) -> None:
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))
ctxMenu.deleteLater()

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."""
Expand Down Expand Up @@ -1875,6 +1853,28 @@ def _insertCommentStructure(self, style: nwComment) -> None:
# 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
Expand Down
15 changes: 7 additions & 8 deletions novelwriter/gui/dochighlight.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,20 +483,19 @@ def spellCheck(self, text: str, offset: int) -> list[tuple[int, int]]:
"""
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
12 changes: 9 additions & 3 deletions tests/test_core/test_core_spellcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 5 additions & 1 deletion tests/test_gui/test_gui_doceditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion tests/test_text/test_text_patterns.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
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)
Expand Down

0 comments on commit 7cc48ae

Please sign in to comment.