From 1ad6a806827a8d89d7ea226a1be5a2193ee84c36 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Fri, 1 Nov 2024 21:17:46 +0100 Subject: [PATCH] Update auto-replace in editor, and update tests --- novelwriter/gui/doceditor.py | 198 ++++++++++-------- .../guiEditor_Main_Final_000000000000f.nwd | 6 +- .../guiEditor_Main_Final_nwProject.nwx | 6 +- tests/test_gui/test_gui_doceditor.py | 4 +- tests/test_gui/test_gui_guimain.py | 30 ++- 5 files changed, 139 insertions(+), 105 deletions(-) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index e2e04e76..feaa912c 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -36,6 +36,7 @@ import logging from enum import Enum from time import time +from typing import NamedTuple from PyQt5.QtCore import ( QObject, QPoint, QRegularExpression, QRunnable, Qt, QTimer, pyqtSignal, @@ -83,6 +84,21 @@ class _SelectAction(Enum): MOVE_AFTER = 3 +class AutoReplaceConfig(NamedTuple): + + typPadChar: str + typSQuoteO: str + typSQuoteC: str + typDQuoteO: str + typDQuoteC: str + typRepDQuote: bool + typRepSQuote: bool + typRepDash: bool + typRepDots: bool + typPadBefore: str + typPadAfter: str + + class GuiDocEditor(QPlainTextEdit): """Gui Widget: Main Document Editor""" @@ -128,17 +144,19 @@ class GuiDocEditor(QPlainTextEdit): self._doReplace = False # Switch to temporarily disable auto-replace # Typography Cache - self._typPadChar = " " - self._typDQuoteO = '"' - self._typDQuoteC = '"' - self._typSQuoteO = "'" - self._typSQuoteC = "'" - self._typRepDQuote = False - self._typRepSQuote = False - self._typRepDash = False - self._typRepDots = False - self._typPadBefore = "" - self._typPadAfter = "" + self._typConf = AutoReplaceConfig( + typPadChar=" ", + typSQuoteO="'", + typSQuoteC="'", + typDQuoteO='"', + typDQuoteC='"', + typRepSQuote=False, + typRepDQuote=False, + typRepDash=False, + typRepDots=False, + typPadBefore="", + typPadAfter="", + ) # Completer self._completer = MetaCompleter(self) @@ -310,21 +328,19 @@ class GuiDocEditor(QPlainTextEdit): created, and when the user changes the main editor preferences. """ # Typography - if CONFIG.fmtPadThin: - self._typPadChar = nwUnicode.U_THNBSP - else: - self._typPadChar = nwUnicode.U_NBSP - - self._typSQuoteO = CONFIG.fmtSQuoteOpen - self._typSQuoteC = CONFIG.fmtSQuoteClose - self._typDQuoteO = CONFIG.fmtDQuoteOpen - self._typDQuoteC = CONFIG.fmtDQuoteClose - self._typRepDQuote = CONFIG.doReplaceDQuote - self._typRepSQuote = CONFIG.doReplaceSQuote - self._typRepDash = CONFIG.doReplaceDash - self._typRepDots = CONFIG.doReplaceDots - self._typPadBefore = CONFIG.fmtPadBefore - self._typPadAfter = CONFIG.fmtPadAfter + self._typConf = AutoReplaceConfig( + typPadChar=nwUnicode.U_THNBSP if CONFIG.fmtPadThin else nwUnicode.U_NBSP, + typSQuoteO=CONFIG.fmtSQuoteOpen, + typSQuoteC=CONFIG.fmtSQuoteClose, + typDQuoteO=CONFIG.fmtDQuoteOpen, + typDQuoteC=CONFIG.fmtDQuoteClose, + typRepSQuote=CONFIG.doReplaceSQuote, + typRepDQuote=CONFIG.doReplaceDQuote, + typRepDash=CONFIG.doReplaceDash, + typRepDots=CONFIG.doReplaceDots, + typPadBefore=CONFIG.fmtPadBefore, + typPadAfter=CONFIG.fmtPadAfter, + ) # Reload spell check and dictionaries SHARED.updateSpellCheckLanguage() @@ -737,6 +753,7 @@ class GuiDocEditor(QPlainTextEdit): logger.debug("Requesting action: %s", action.name) + tConf = self._typConf self._allowAutoReplace(False) if action == nwDocAction.UNDO: self.undo() @@ -755,9 +772,9 @@ class GuiDocEditor(QPlainTextEdit): elif action == nwDocAction.MD_STRIKE: self._toggleFormat(2, "~") elif action == nwDocAction.S_QUOTE: - self._wrapSelection(self._typSQuoteO, self._typSQuoteC) + self._wrapSelection(tConf.typSQuoteO, tConf.typSQuoteC) elif action == nwDocAction.D_QUOTE: - self._wrapSelection(self._typDQuoteO, self._typDQuoteC) + self._wrapSelection(tConf.typDQuoteO, tConf.typDQuoteC) elif action == nwDocAction.SEL_ALL: self._makeSelection(QTextCursor.SelectionType.Document) elif action == nwDocAction.SEL_PARA: @@ -783,9 +800,9 @@ class GuiDocEditor(QPlainTextEdit): elif action == nwDocAction.BLOCK_HSC: self._formatBlock(nwDocAction.BLOCK_HSC) elif action == nwDocAction.REPL_SNG: - self._replaceQuotes("'", self._typSQuoteO, self._typSQuoteC) + self._replaceQuotes("'", tConf.typSQuoteO, tConf.typSQuoteC) elif action == nwDocAction.REPL_DBL: - self._replaceQuotes("\"", self._typDQuoteO, self._typDQuoteC) + self._replaceQuotes("\"", tConf.typDQuoteO, tConf.typDQuoteC) elif action == nwDocAction.RM_BREAKS: self._removeInParLineBreaks() elif action == nwDocAction.ALIGN_L: @@ -857,13 +874,13 @@ class GuiDocEditor(QPlainTextEdit): text = insert elif isinstance(insert, nwDocInsert): if insert == nwDocInsert.QUOTE_LS: - text = self._typSQuoteO + text = self._typConf.typSQuoteO elif insert == nwDocInsert.QUOTE_RS: - text = self._typSQuoteC + text = self._typConf.typSQuoteC elif insert == nwDocInsert.QUOTE_LD: - text = self._typDQuoteO + text = self._typConf.typDQuoteO elif insert == nwDocInsert.QUOTE_RD: - text = self._typDQuoteC + text = self._typConf.typDQuoteC elif insert == nwDocInsert.SYNOPSIS: text = "%Synopsis: " block = True @@ -1986,89 +2003,90 @@ class GuiDocEditor(QPlainTextEdit): if not t1: return - nDelete = 0 - tInsert = t1 + delete = 0 + insert = t1 + tConf = self._typConf - if self._typRepDQuote and t2[:1].isspace() and t2.endswith('"'): - nDelete = 1 - tInsert = self._typDQuoteO + if tConf.typRepDQuote and t2[:1].isspace() and t2.endswith('"'): + delete = 1 + insert = tConf.typDQuoteO - elif self._typRepDQuote and t1 == '"': - nDelete = 1 + elif tConf.typRepDQuote and t1 == '"': + delete = 1 if tPos == 1: - tInsert = self._typDQuoteO + insert = tConf.typDQuoteO elif tPos == 2 and t2 == '>"': - tInsert = self._typDQuoteO + insert = tConf.typDQuoteO elif tPos == 3 and t3 == '>>"': - tInsert = self._typDQuoteO + insert = tConf.typDQuoteO else: - tInsert = self._typDQuoteC + insert = tConf.typDQuoteC - elif self._typRepSQuote and t2[:1].isspace() and t2.endswith("'"): - nDelete = 1 - tInsert = self._typSQuoteO + elif tConf.typRepSQuote and t2[:1].isspace() and t2.endswith("'"): + delete = 1 + insert = tConf.typSQuoteO - elif self._typRepSQuote and t1 == "'": - nDelete = 1 + elif tConf.typRepSQuote and t1 == "'": + delete = 1 if tPos == 1: - tInsert = self._typSQuoteO + insert = tConf.typSQuoteO elif tPos == 2 and t2 == ">'": - tInsert = self._typSQuoteO + insert = tConf.typSQuoteO elif tPos == 3 and t3 == ">>'": - tInsert = self._typSQuoteO + insert = tConf.typSQuoteO else: - tInsert = self._typSQuoteC + insert = tConf.typSQuoteC - elif self._typRepDash and t4 == "----": - nDelete = 4 - tInsert = nwUnicode.U_HBAR + elif tConf.typRepDash and t4 == "----": + delete = 4 + insert = nwUnicode.U_HBAR - elif self._typRepDash and t3 == "---": - nDelete = 3 - tInsert = nwUnicode.U_EMDASH + elif tConf.typRepDash and t3 == "---": + delete = 3 + insert = nwUnicode.U_EMDASH - elif self._typRepDash and t2 == "--": - nDelete = 2 - tInsert = nwUnicode.U_ENDASH + elif tConf.typRepDash and t2 == "--": + delete = 2 + insert = nwUnicode.U_ENDASH - elif self._typRepDash and t2 == nwUnicode.U_ENDASH + "-": - nDelete = 2 - tInsert = nwUnicode.U_EMDASH + elif tConf.typRepDash and t2 == nwUnicode.U_ENDASH + "-": + delete = 2 + insert = nwUnicode.U_EMDASH - elif self._typRepDash and t2 == nwUnicode.U_EMDASH + "-": - nDelete = 2 - tInsert = nwUnicode.U_HBAR + elif tConf.typRepDash and t2 == nwUnicode.U_EMDASH + "-": + delete = 2 + insert = nwUnicode.U_HBAR - elif self._typRepDots and t3 == "...": - nDelete = 3 - tInsert = nwUnicode.U_HELLIP + elif tConf.typRepDots and t3 == "...": + delete = 3 + insert = nwUnicode.U_HELLIP elif t1 == nwUnicode.U_LSEP: # This resolves issue #1150 - nDelete = 1 - tInsert = nwUnicode.U_PSEP + delete = 1 + insert = nwUnicode.U_PSEP - tCheck = tInsert - if self._typPadBefore and tCheck in self._typPadBefore: - if self._allowSpaceBeforeColon(text, tCheck): - nDelete = max(nDelete, 1) - chkPos = tPos - nDelete - 1 + check = insert + if tConf.typPadBefore and check in tConf.typPadBefore: + if self._allowSpaceBeforeColon(text, check): + delete = max(delete, 1) + chkPos = tPos - delete - 1 if chkPos >= 0 and text[chkPos].isspace(): # Strip existing space before inserting a new (#1061) - nDelete += 1 - tInsert = self._typPadChar + tInsert + delete += 1 + insert = tConf.typPadChar + insert - if self._typPadAfter and tCheck in self._typPadAfter: - if self._allowSpaceBeforeColon(text, tCheck): - nDelete = max(nDelete, 1) - tInsert = tInsert + self._typPadChar + if tConf.typPadAfter and check in tConf.typPadAfter: + if self._allowSpaceBeforeColon(text, check): + delete = max(delete, 1) + insert = insert + tConf.typPadChar - if nDelete > 0: - cursor.movePosition(QtMoveLeft, QtKeepAnchor, nDelete) - cursor.insertText(tInsert) + if delete > 0: + cursor.movePosition(QtMoveLeft, QtKeepAnchor, delete) + cursor.insertText(insert) - # Re-highlight, since the auto-replace sometimes interferes with it - self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block()) + # Re-highlight, since the auto-replace sometimes interferes with it + self._qDocument.syntaxHighlighter.rehighlightBlock(cursor.block()) return diff --git a/tests/reference/guiEditor_Main_Final_000000000000f.nwd b/tests/reference/guiEditor_Main_Final_000000000000f.nwd index fe96f10a..e59f324e 100644 --- a/tests/reference/guiEditor_Main_Final_000000000000f.nwd +++ b/tests/reference/guiEditor_Main_Final_000000000000f.nwd @@ -1,8 +1,8 @@ %%~name: New Scene %%~path: 000000000000d/000000000000f %%~kind: NOVEL/DOCUMENT -%%~hash: 89b54ddaec2fddfd220e91dd438c84fb3aef9fc2 -%%~date: 2024-10-30 00:07:21/2024-10-30 00:07:26 +%%~hash: e4148ea77e78c90c334d5dc46c38a2b7904ac117 +%%~date: 2024-11-01 21:15:57/2024-11-01 21:16:01 # Novel ## Chapter @@ -26,7 +26,7 @@ This is a paragraph of nonsense text. This is another paragraph with a line separator in it. -This is another paragraph of much longer nonsense text. It is in fact 1 very very NONSENSICAL nonsense text! We can also try replacing “quotes”, even single ‘quotes’ are replaced. Isn’t that nice? We can hyphen-ate, make dashes – and even longer dashes — if we want. Ellipsis? Not a problem either … How about three hyphens — for long dash? It works too. +This is another paragraph of much longer nonsense text. It is in fact 1 very very NONSENSICAL nonsense text! We can also try replacing “quotes”, even single ‘quotes’ are replaced. Isn’t that nice? We can hyphen-ate, make dashes – and even longer dashes — if we want. We can even go on to a ― hotizontal bar. Ellipsis? Not a problem either … How about three hyphens — for long dash? It works too. Even four hyphens ― for a horizontal works! “Full line double quoted text.” diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 66c1939d..4a06631a 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -28,7 +28,7 @@ Main - + Novel @@ -46,7 +46,7 @@ New Chapter - + New Scene diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 62c60859..0f1fc5c2 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -80,7 +80,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert qDoc.defaultTextOption().alignment() == QtAlignLeft assert docEditor.verticalScrollBarPolicy() == QtScrollAsNeeded assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded - assert docEditor._typPadChar == nwUnicode.U_NBSP + assert docEditor._typConf.typPadChar == nwUnicode.U_NBSP assert docEditor.docHeader.itemTitle.text() == ( "Novel \u203a New Chapter \u203a New Scene" ) @@ -105,7 +105,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert qDoc.defaultTextOption().flags() & QTextOption.ShowLineAndParagraphSeparators assert docEditor.verticalScrollBarPolicy() == QtScrollAlwaysOff assert docEditor.horizontalScrollBarPolicy() == QtScrollAlwaysOff - assert docEditor._typPadChar == nwUnicode.U_THNBSP + assert docEditor._typConf.typPadChar == nwUnicode.U_THNBSP assert docEditor.docHeader.itemTitle.text() == "New Scene" # Header diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 622d81b6..6d151b81 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -421,6 +421,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "We can hyphen-ate, make dashes -- and even longer dashes --- if we want. ": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) + for c in "We can even go on to a ---- hotizontal bar. ": + qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "Ellipsis? Not a problem either ... ": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) for c in "How about three hyphens - -": @@ -428,7 +430,17 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): qtbot.keyClick(docEditor, Qt.Key.Key_Left, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Right, delay=KEY_DELAY) - for c in "- for long dash? It works too.": + for c in "- for long dash? It works too. ": + qtbot.keyClick(docEditor, c, delay=KEY_DELAY) + for c in "Even four hyphens - - -": + qtbot.keyClick(docEditor, c, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Left, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Left, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Backspace, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Right, delay=KEY_DELAY) + qtbot.keyClick(docEditor, Qt.Key.Key_Right, delay=KEY_DELAY) + for c in "- for a horizontal works!": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) @@ -447,16 +459,18 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) # Insert spaces before and after quotes - docEditor._typPadBefore = "\u201d" - docEditor._typPadAfter = "\u201c" + CONFIG.fmtPadBefore = "\u201d" + CONFIG.fmtPadAfter = "\u201c" + docEditor.initEditor() for c in "Some \"double quoted text with spaces padded\".": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) - docEditor._typPadBefore = "" - docEditor._typPadAfter = "" + CONFIG.fmtPadBefore = "" + CONFIG.fmtPadAfter = "" + docEditor.initEditor() # Dialogue Line for c in "-- Hi, I am a character speaking.": @@ -478,7 +492,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): # ================== # Insert spaces before colon, but ignore tags - docEditor._typPadBefore = ":" + CONFIG.fmtPadBefore = ":" + docEditor.initEditor() for c in "@object: NoSpaceAdded": qtbot.keyClick(docEditor, c, delay=KEY_DELAY) @@ -505,7 +520,8 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) qtbot.keyClick(docEditor, Qt.Key.Key_Return, delay=KEY_DELAY) - docEditor._typPadBefore = "" + CONFIG.fmtPadBefore = "" + docEditor.initEditor() # Indent and Align # ================