diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 13525ded..0b0e7568 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -1470,7 +1470,7 @@ class GuiDocEditor(QPlainTextEdit): # Internal Functions : Text Manipulation ## - def _toggleFormat(self, fLen: int, fChar: str) -> bool: + def _toggleFormat(self, fLen: int, fChar: str) -> None: """Toggle the formatting of a specific type for a piece of text. If more than one block is selected, the formatting is applied to the first block. @@ -1488,12 +1488,12 @@ class GuiDocEditor(QPlainTextEdit): posS = cursor.selectionStart() posE = cursor.selectionEnd() - if self._qDocument.characterAt(posO - 1) == fChar: + if posS == posE and self._qDocument.characterAt(posO - 1) == fChar: logger.warning("Format repetition, cancelling action") cursor.clearSelection() cursor.setPosition(posO) self.setTextCursor(cursor) - return False + return blockS = self._qDocument.findBlock(posS) blockE = self._qDocument.findBlock(posE) @@ -1519,7 +1519,6 @@ class GuiDocEditor(QPlainTextEdit): break if fLen == min(numA, numB): - cursor.clearSelection() cursor.beginEditBlock() cursor.setPosition(posS) for i in range(fLen): @@ -1528,17 +1527,19 @@ class GuiDocEditor(QPlainTextEdit): for i in range(fLen): cursor.deletePreviousChar() cursor.endEditBlock() - cursor.clearSelection() - cursor.setPosition(posO - fLen) - self.setTextCursor(cursor) + + if select != _SelectAction.KEEP_SELECTION: + cursor.clearSelection() + cursor.setPosition(posO - fLen) + self.setTextCursor(cursor) else: self._wrapSelection(fChar*fLen, pos=posO, select=select) - return True + return def _wrapSelection(self, before: str, after: str | None = None, pos: int | None = None, - select: _SelectAction = _SelectAction.NO_DECISION) -> bool: + select: _SelectAction = _SelectAction.NO_DECISION) -> None: """Wrap the selected text in whatever is in tBefore and tAfter. If there is no selection, the autoSelect setting decides the action. AutoSelect will select the word under the cursor before @@ -1578,21 +1579,21 @@ class GuiDocEditor(QPlainTextEdit): if select == _SelectAction.MOVE_AFTER: cursor.setPosition(posE + len(before + after)) elif select == _SelectAction.KEEP_SELECTION: - cursor.setPosition(posE + len(before), QtMoveAnchor) - cursor.setPosition(posS + len(before), QtKeepAnchor) + cursor.setPosition(posS + len(before), QtMoveAnchor) + cursor.setPosition(posE + len(before), QtKeepAnchor) elif select == _SelectAction.KEEP_POSITION: cursor.setPosition(posO + len(before)) self.setTextCursor(cursor) - return True + return - def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> bool: + def _replaceQuotes(self, sQuote: str, oQuote: str, cQuote: str) -> None: """Replace all straight quotes in the selected text.""" cursor = self.textCursor() if not cursor.hasSelection(): SHARED.error(self.tr("Please select some text before calling replace quotes.")) - return False + return posS = cursor.selectionStart() posE = cursor.selectionEnd() @@ -1632,7 +1633,7 @@ class GuiDocEditor(QPlainTextEdit): self._allowAutoReplace(True) - return True + return def _processBlockFormat( self, action: nwDocAction, text: str, toggle: bool = True diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index beba03f0..86cc0d50 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -949,6 +949,15 @@ def testCoreToken_ExtractFormats(mockGUI): (10, tokens.FMT_B_B), (31, tokens.FMT_I_B), (38, tokens.FMT_B_E), (38, tokens.FMT_I_E) ] + # So does this + text, fmt = tokens._extractFormats( + "Text with [b]bold and overlapping [i]italics[/b] in[/i] it." + ) + assert text == "Text with bold and overlapping italics in it." + assert fmt == [ + (10, tokens.FMT_B_B), (31, tokens.FMT_I_B), (38, tokens.FMT_B_E), (41, tokens.FMT_I_E) + ] + # END Test testCoreToken_ExtractFormats diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index cfcb8f68..be409df2 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -880,13 +880,13 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): # Wrap Equal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._wrapSelection("=") is True + nwGUI.docEditor._wrapSelection("=") assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur=") # Wrap Unequal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._wrapSelection("=", "*") is True + nwGUI.docEditor._wrapSelection("=", "*") assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur*") # Past Paragraph @@ -895,7 +895,7 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): cursor.setPosition(13, QTextCursor.MoveAnchor) cursor.setPosition(1000, QTextCursor.KeepAnchor) nwGUI.docEditor.setTextCursor(cursor) - assert nwGUI.docEditor._wrapSelection("=") is True + nwGUI.docEditor._wrapSelection("=") newText = nwGUI.docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) @@ -910,14 +910,15 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): # Block format repetition nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(39) - assert nwGUI.docEditor._toggleFormat(1, "=") is True + nwGUI.docEditor._toggleFormat(1, "=") + assert nwGUI.docEditor.getText() == text.replace("amet", "=amet=", 1) + nwGUI.docEditor._toggleFormat(1, "=") assert nwGUI.docEditor.getText() == text.replace("amet", "=amet=", 1) - assert nwGUI.docEditor._toggleFormat(1, "=") is False # Wrap Single Equal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._toggleFormat(1, "=") is True + nwGUI.docEditor._toggleFormat(1, "=") assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur=") # Past Paragraph @@ -926,7 +927,7 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): cursor.setPosition(13, QTextCursor.MoveAnchor) cursor.setPosition(1000, QTextCursor.KeepAnchor) nwGUI.docEditor.setTextCursor(cursor) - assert nwGUI.docEditor._toggleFormat(1, "=") is True + nwGUI.docEditor._toggleFormat(1, "=") newText = nwGUI.docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) @@ -936,30 +937,40 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): # Wrap Double Equal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._toggleFormat(2, "=") is True + nwGUI.docEditor._toggleFormat(2, "=") assert nwGUI.docEditor.getText() == text.replace("consectetur", "==consectetur==") + # Toggle Double Equal with Selection + nwGUI.docEditor.replaceText(text) + nwGUI.docEditor.setCursorSelection(41, 11) + nwGUI.docEditor._toggleFormat(2, "=") + assert nwGUI.docEditor.getText() == text.replace("consectetur", "==consectetur==") + assert nwGUI.docEditor.getSelectedText() == "consectetur" + nwGUI.docEditor._toggleFormat(2, "=") + assert nwGUI.docEditor.getText() == text + assert nwGUI.docEditor.getSelectedText() == "consectetur" + # Toggle Double Equal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._toggleFormat(2, "=") is True - assert nwGUI.docEditor._toggleFormat(2, "=") is True + nwGUI.docEditor._toggleFormat(2, "=") + nwGUI.docEditor._toggleFormat(2, "=") assert nwGUI.docEditor.getText() == text # Toggle Triple+Double Equal nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._toggleFormat(3, "=") is True - assert nwGUI.docEditor._toggleFormat(2, "=") is True + nwGUI.docEditor._toggleFormat(3, "=") + nwGUI.docEditor._toggleFormat(2, "=") assert nwGUI.docEditor.getText() == text.replace("consectetur", "=consectetur=") # Toggle Unequal repText = text.replace("consectetur", "=consectetur==") nwGUI.docEditor.replaceText(repText) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._toggleFormat(1, "=") is True + nwGUI.docEditor._toggleFormat(1, "=") assert nwGUI.docEditor.getText() == text.replace("consectetur", "consectetur=") - assert nwGUI.docEditor._toggleFormat(1, "=") is True + nwGUI.docEditor._toggleFormat(1, "=") assert nwGUI.docEditor.getText() == repText # Replace Quotes @@ -969,7 +980,8 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): text = "### A Scene\n\n%s" % ipsumText[0].replace("consectetur", "=consectetur=") nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) - assert nwGUI.docEditor._replaceQuotes("=", "<", ">") is False + nwGUI.docEditor._replaceQuotes("=", "<", ">") + assert nwGUI.docEditor.getText() == text # First Paragraph Selected # This should not replace anything in second paragraph @@ -977,7 +989,7 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor.docAction(nwDocAction.SEL_PARA) - assert nwGUI.docEditor._replaceQuotes("=", "<", ">") is True + nwGUI.docEditor._replaceQuotes("=", "<", ">") newText = nwGUI.docEditor.getText() newPara = list(filter(str.strip, newText.split("\n"))) @@ -989,7 +1001,7 @@ def testGuiEditor_TextManipulation(qtbot, nwGUI, projPath, ipsumText, mockRnd): nwGUI.docEditor.replaceText(text) nwGUI.docEditor.setCursorPosition(45) assert nwGUI.docEditor.docAction(nwDocAction.SEL_ALL) - assert nwGUI.docEditor._replaceQuotes("=", "<", ">") is True + nwGUI.docEditor._replaceQuotes("=", "<", ">") assert nwGUI.docEditor.getText() == text.replace("=Lorem=", "") # Remove Line Breaks