Update auto-replace in editor, and update tests
This commit is contained in:
+106
-88
@@ -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,86 +2003,87 @@ 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())
|
||||
|
||||
@@ -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.”
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-10-30 00:06:45">
|
||||
<novelWriterXML appVersion="2.6a3" hexVersion="0x020600a3" fileVersion="1.5" fileRevision="4" timeStamp="2024-11-01 21:15:11">
|
||||
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="3" autoCount="2" editTime="5">
|
||||
<name>New Project</name>
|
||||
<author>Jane Doe</author>
|
||||
@@ -28,7 +28,7 @@
|
||||
<entry key="i000007" count="0" red="50" green="200" blue="0" shape="SQUARE">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content items="11" novelWords="161" notesWords="27">
|
||||
<content items="11" novelWords="179" notesWords="27">
|
||||
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="yes" />
|
||||
<name status="s000000" import="i000004">Novel</name>
|
||||
@@ -46,7 +46,7 @@
|
||||
<name status="s000000" import="i000004" active="yes">New Chapter</name>
|
||||
</item>
|
||||
<item handle="000000000000f" parent="000000000000d" root="0000000000008" order="1" type="FILE" class="NOVEL" layout="DOCUMENT">
|
||||
<meta expanded="no" heading="H1" charCount="918" wordCount="154" paraCount="17" cursorPos="1174" />
|
||||
<meta expanded="no" heading="H1" charCount="1003" wordCount="172" paraCount="17" cursorPos="1259" />
|
||||
<name status="s000000" import="i000004" active="yes">New Scene</name>
|
||||
</item>
|
||||
<item handle="0000000000009" parent="None" root="0000000000009" order="1" type="ROOT" class="PLOT">
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 - -":
|
||||
@@ -430,6 +432,16 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
||||
qtbot.keyClick(docEditor, Qt.Key.Key_Right, delay=KEY_DELAY)
|
||||
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
|
||||
# ================
|
||||
|
||||
Reference in New Issue
Block a user