Fix various behaviour inconsistencies
This commit is contained in:
@@ -1249,8 +1249,8 @@ class GuiDocEditor(QTextEdit):
|
||||
if key == QtKeyEscape:
|
||||
if self.searchVisible():
|
||||
self.closeSearch()
|
||||
elif self._vimMode and self._vimMode.mode != nwVimMode.NORMAL:
|
||||
self._vimMode.setVimMode(nwVimMode.NORMAL)
|
||||
elif self._vimMode and self._vimMode.cancel():
|
||||
pass # Vim mode consumed the key
|
||||
elif SHARED.focusMode:
|
||||
SHARED.setFocusMode(False)
|
||||
event.ignore()
|
||||
|
||||
@@ -28,10 +28,18 @@ from typing import TYPE_CHECKING
|
||||
from novelwriter.enum import nwDocAction, nwVimMode
|
||||
from novelwriter.types import (
|
||||
QtKeepAnchor,
|
||||
QtKeyDown,
|
||||
QtKeyEnd,
|
||||
QtKeyHome,
|
||||
QtKeyLeft,
|
||||
QtKeyPageDown,
|
||||
QtKeyPageUp,
|
||||
QtKeyRight,
|
||||
QtKeyUp,
|
||||
QtModShift,
|
||||
QtMoveDown,
|
||||
QtMoveEnd,
|
||||
QtMoveEndOfBlock,
|
||||
QtMoveEndOfLine,
|
||||
QtMoveEndOfWord,
|
||||
QtMoveLeft,
|
||||
QtMoveNextChar,
|
||||
@@ -40,9 +48,7 @@ from novelwriter.types import (
|
||||
QtMoveRight,
|
||||
QtMoveStart,
|
||||
QtMoveStartOfBlock,
|
||||
QtMoveStartOfLine,
|
||||
QtMoveUp,
|
||||
QtSelectLine,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -54,6 +60,8 @@ if TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
NAV_KEYS = (QtKeyLeft, QtKeyRight, QtKeyUp, QtKeyDown, QtKeyHome, QtKeyEnd, QtKeyPageUp, QtKeyPageDown)
|
||||
|
||||
|
||||
class EditorVimMode:
|
||||
"""Editor Component: Vim Mode."""
|
||||
@@ -66,15 +74,6 @@ class EditorVimMode:
|
||||
self._vim = VimState()
|
||||
self._callback = callback
|
||||
|
||||
##
|
||||
# Properties
|
||||
##
|
||||
|
||||
@property
|
||||
def mode(self) -> nwVimMode:
|
||||
"""Return the current vim mode."""
|
||||
return self._vim.mode
|
||||
|
||||
##
|
||||
# Setters
|
||||
##
|
||||
@@ -100,14 +99,27 @@ class EditorVimMode:
|
||||
"""Return a new text cursor."""
|
||||
return self._editor.textCursor()
|
||||
|
||||
def cancel(self) -> bool:
|
||||
"""Cancel the current mode or pending command, if any."""
|
||||
if self._vim.mode != nwVimMode.NORMAL:
|
||||
self.setVimMode(nwVimMode.NORMAL)
|
||||
return True
|
||||
elif self._vim.command:
|
||||
self._vim.resetCommand()
|
||||
return True
|
||||
return False
|
||||
|
||||
def processVimKeyEvent(self, event: QKeyEvent) -> bool:
|
||||
"""Process a keypress for vim actions."""
|
||||
if self._vim.mode == nwVimMode.INSERT or self._editor.isReadOnly():
|
||||
return False
|
||||
elif self._handleVimNormalModeModeSwitching(event):
|
||||
return True
|
||||
elif self._vim.mode == nwVimMode.NORMAL and event.key() in NAV_KEYS and not event.modifiers() & QtModShift:
|
||||
self._vim.resetCommand()
|
||||
return False
|
||||
elif self._vim.mode in (nwVimMode.VISUAL, nwVimMode.V_LINE):
|
||||
self._handleVimVisualMode(event)
|
||||
elif self._handleVimNormalModeModeSwitching(event):
|
||||
return True
|
||||
else:
|
||||
self._handleVimNormalMode(event)
|
||||
return True
|
||||
@@ -123,7 +135,7 @@ class EditorVimMode:
|
||||
return True
|
||||
elif text == "I":
|
||||
cursor = self.textCursor()
|
||||
cursor.movePosition(QtMoveStartOfLine)
|
||||
cursor.movePosition(QtMoveStartOfBlock)
|
||||
self.setTextCursor(cursor)
|
||||
self.setVimMode(nwVimMode.INSERT)
|
||||
return True
|
||||
@@ -136,7 +148,7 @@ class EditorVimMode:
|
||||
elif text == "V":
|
||||
self.setVimMode(nwVimMode.V_LINE)
|
||||
cursor = self.textCursor()
|
||||
cursor.select(QtSelectLine)
|
||||
self._selectBlocks(cursor)
|
||||
self.setTextCursor(cursor)
|
||||
return True
|
||||
return False # Not a mode switching motion
|
||||
@@ -152,24 +164,16 @@ class EditorVimMode:
|
||||
self._vim.setCommand(key)
|
||||
|
||||
if (command := self._vim.command) == "dd":
|
||||
cursor.beginEditBlock()
|
||||
cursor.movePosition(QtMoveStartOfBlock)
|
||||
cursor.movePosition(QtMoveEndOfBlock, QtKeepAnchor)
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
cursor.removeSelectedText()
|
||||
if cursor.block().next().isValid():
|
||||
cursor.deleteChar()
|
||||
else:
|
||||
cursor.deletePreviousChar()
|
||||
cursor.endEditBlock()
|
||||
self.setTextCursor(cursor)
|
||||
self._selectBlocks(cursor)
|
||||
self._deleteBlocks(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "x":
|
||||
cursor.movePosition(QtMoveRight, QtKeepAnchor)
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
cursor.removeSelectedText()
|
||||
self.setTextCursor(cursor)
|
||||
if not cursor.atBlockEnd():
|
||||
cursor.movePosition(QtMoveRight, QtKeepAnchor)
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
cursor.removeSelectedText()
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "w":
|
||||
@@ -197,7 +201,10 @@ class EditorVimMode:
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "dw":
|
||||
blockEnd = cursor.block().position() + cursor.block().length() - 1
|
||||
cursor.movePosition(QtMoveNextWord, QtKeepAnchor)
|
||||
if cursor.position() > blockEnd:
|
||||
cursor.setPosition(blockEnd, QtKeepAnchor)
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
cursor.removeSelectedText()
|
||||
self.setTextCursor(cursor)
|
||||
@@ -248,23 +255,29 @@ class EditorVimMode:
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "yy":
|
||||
cursor.select(QtSelectLine)
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
cursor.clearSelection()
|
||||
self.setTextCursor(cursor)
|
||||
self._selectBlocks(cursor)
|
||||
self._vim.yankToInternal(cursor.selectedText(), lineWise=True)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "p":
|
||||
if text := self._vim.pasteFromInternal():
|
||||
cursor.movePosition(QtMoveEndOfBlock)
|
||||
cursor.insertText("\n" + text)
|
||||
if self._vim.lineWise:
|
||||
cursor.movePosition(QtMoveEndOfBlock)
|
||||
cursor.insertText("\n" + text)
|
||||
else:
|
||||
if not cursor.atBlockEnd():
|
||||
cursor.movePosition(QtMoveRight)
|
||||
cursor.insertText(text)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "P":
|
||||
if text := self._vim.pasteFromInternal():
|
||||
cursor.movePosition(QtMoveStartOfBlock)
|
||||
cursor.insertText(text + "\n")
|
||||
if self._vim.lineWise:
|
||||
cursor.movePosition(QtMoveStartOfBlock)
|
||||
cursor.insertText(text + "\n")
|
||||
else:
|
||||
cursor.insertText(text)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
@@ -282,12 +295,12 @@ class EditorVimMode:
|
||||
self.setVimMode(nwVimMode.INSERT)
|
||||
|
||||
elif command == "$":
|
||||
cursor.movePosition(QtMoveEndOfLine)
|
||||
cursor.movePosition(QtMoveEndOfBlock)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "0":
|
||||
cursor.movePosition(QtMoveStartOfLine)
|
||||
cursor.movePosition(QtMoveStartOfBlock)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
@@ -297,7 +310,7 @@ class EditorVimMode:
|
||||
self.setVimMode(nwVimMode.INSERT)
|
||||
|
||||
elif command == "A":
|
||||
cursor.movePosition(QtMoveEndOfLine)
|
||||
cursor.movePosition(QtMoveEndOfBlock)
|
||||
self.setTextCursor(cursor)
|
||||
self.setVimMode(nwVimMode.INSERT)
|
||||
|
||||
@@ -311,8 +324,9 @@ class EditorVimMode:
|
||||
|
||||
# Single-step navigation
|
||||
elif command == "h":
|
||||
cursor.movePosition(QtMoveLeft)
|
||||
self.setTextCursor(cursor)
|
||||
if not cursor.atBlockStart():
|
||||
cursor.movePosition(QtMoveLeft)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "j":
|
||||
@@ -326,8 +340,9 @@ class EditorVimMode:
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "l":
|
||||
cursor.movePosition(QtMoveRight)
|
||||
self.setTextCursor(cursor)
|
||||
if not cursor.atBlockEnd():
|
||||
cursor.movePosition(QtMoveRight)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif len(command) > 1 or command not in self._vim.PREFIX_KEYS:
|
||||
@@ -348,16 +363,32 @@ class EditorVimMode:
|
||||
self._vim.setCommand(key)
|
||||
|
||||
# --- VISUAL / VISUALLINE mode ---
|
||||
lineWise = self._vim.mode == nwVimMode.V_LINE
|
||||
if (command := self._vim.command) in ("d", "x"):
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
cursor.removeSelectedText()
|
||||
self.setTextCursor(cursor)
|
||||
if lineWise:
|
||||
self._deleteBlocks(cursor)
|
||||
else:
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
cursor.removeSelectedText()
|
||||
self.setTextCursor(cursor)
|
||||
self.setVimMode(nwVimMode.NORMAL)
|
||||
|
||||
elif command == "y":
|
||||
self._vim.yankToInternal(cursor.selectedText())
|
||||
self._vim.yankToInternal(cursor.selectedText(), lineWise=lineWise)
|
||||
self.setVimMode(nwVimMode.NORMAL)
|
||||
|
||||
elif command == "v":
|
||||
if self._vim.mode == nwVimMode.VISUAL:
|
||||
self.setVimMode(nwVimMode.NORMAL)
|
||||
else:
|
||||
self.setVimMode(nwVimMode.VISUAL)
|
||||
|
||||
elif command == "V":
|
||||
if self._vim.mode == nwVimMode.V_LINE:
|
||||
self.setVimMode(nwVimMode.NORMAL)
|
||||
else:
|
||||
self.setVimMode(nwVimMode.V_LINE)
|
||||
|
||||
elif command == "w":
|
||||
cursor.movePosition(QtMoveNextWord, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
@@ -385,13 +416,15 @@ class EditorVimMode:
|
||||
|
||||
# Handle motions (extend selection)
|
||||
elif command == "h":
|
||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
if not cursor.atBlockStart():
|
||||
cursor.movePosition(QtMoveLeft, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "l":
|
||||
cursor.movePosition(QtMoveRight, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
if not cursor.atBlockEnd():
|
||||
cursor.movePosition(QtMoveRight, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "j":
|
||||
@@ -405,12 +438,12 @@ class EditorVimMode:
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "$":
|
||||
cursor.movePosition(QtMoveEndOfLine, QtKeepAnchor)
|
||||
cursor.movePosition(QtMoveEndOfBlock, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
elif command == "0":
|
||||
cursor.movePosition(QtMoveStartOfLine, QtKeepAnchor)
|
||||
cursor.movePosition(QtMoveStartOfBlock, QtKeepAnchor)
|
||||
self.setTextCursor(cursor)
|
||||
self._vim.resetCommand()
|
||||
|
||||
@@ -423,11 +456,39 @@ class EditorVimMode:
|
||||
# Not a known command or a pending prefix
|
||||
self._vim.resetCommand()
|
||||
|
||||
# V-LINE mode always selects whole lines
|
||||
if self._vim.mode == nwVimMode.V_LINE:
|
||||
cursor = self.textCursor()
|
||||
self._selectBlocks(cursor)
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
def _selectBlocks(self, cursor: QTextCursor) -> None:
|
||||
"""Extend the selection to whole blocks, keeping its direction."""
|
||||
forward = cursor.anchor() <= cursor.position()
|
||||
position = cursor.position()
|
||||
cursor.setPosition(cursor.anchor())
|
||||
cursor.movePosition(QtMoveStartOfBlock if forward else QtMoveEndOfBlock)
|
||||
cursor.setPosition(position, QtKeepAnchor)
|
||||
cursor.movePosition(QtMoveEndOfBlock if forward else QtMoveStartOfBlock, QtKeepAnchor)
|
||||
|
||||
def _deleteBlocks(self, cursor: QTextCursor) -> None:
|
||||
"""Delete the selected blocks, including the block separator."""
|
||||
cursor.beginEditBlock()
|
||||
self._vim.yankToInternal(cursor.selectedText(), lineWise=True)
|
||||
cursor.removeSelectedText()
|
||||
if cursor.block().next().isValid():
|
||||
cursor.deleteChar()
|
||||
else:
|
||||
cursor.deletePreviousChar()
|
||||
cursor.movePosition(QtMoveStartOfBlock)
|
||||
cursor.endEditBlock()
|
||||
self.setTextCursor(cursor)
|
||||
|
||||
|
||||
class VimState:
|
||||
"""Minimal Vim state machine."""
|
||||
|
||||
__slots__ = ("_buffer", "_command", "_mode")
|
||||
__slots__ = ("_buffer", "_command", "_lineWise", "_mode")
|
||||
|
||||
PREFIX_KEYS = "dygz"
|
||||
VISUAL_PREFIX_KEYS = "g"
|
||||
@@ -436,6 +497,7 @@ class VimState:
|
||||
def __init__(self) -> None:
|
||||
self._mode: nwVimMode = nwVimMode.NORMAL
|
||||
self._buffer = ""
|
||||
self._lineWise = False
|
||||
self._command = ""
|
||||
|
||||
@property
|
||||
@@ -448,6 +510,11 @@ class VimState:
|
||||
"""Return the current vim command."""
|
||||
return self._command
|
||||
|
||||
@property
|
||||
def lineWise(self) -> bool:
|
||||
"""Return True if the vim buffer holds whole lines."""
|
||||
return self._lineWise
|
||||
|
||||
def setMode(self, mode: nwVimMode) -> None:
|
||||
"""Switch vim mode."""
|
||||
if mode != self._mode:
|
||||
@@ -469,9 +536,10 @@ class VimState:
|
||||
if self._mode != nwVimMode.INSERT:
|
||||
self._command = key
|
||||
|
||||
def yankToInternal(self, text: str) -> None:
|
||||
def yankToInternal(self, text: str, lineWise: bool = False) -> None:
|
||||
"""Put text into internal vim buffer."""
|
||||
self._buffer = text
|
||||
self._lineWise = lineWise
|
||||
|
||||
def pasteFromInternal(self) -> str:
|
||||
"""Paste from the internal vim buffer."""
|
||||
|
||||
@@ -129,6 +129,8 @@ QtKeyUp = Qt.Key.Key_Up
|
||||
QtKeyDown = Qt.Key.Key_Down
|
||||
QtKeyPageUp = Qt.Key.Key_PageUp
|
||||
QtKeyPageDown = Qt.Key.Key_PageDown
|
||||
QtKeyHome = Qt.Key.Key_Home
|
||||
QtKeyEnd = Qt.Key.Key_End
|
||||
QtKeyTab = Qt.Key.Key_Tab
|
||||
QtKeyEscape = Qt.Key.Key_Escape
|
||||
QtKeyBackspace = Qt.Key.Key_Backspace
|
||||
|
||||
@@ -33,7 +33,7 @@ from novelwriter.editor.editor import GuiDocEditor
|
||||
from novelwriter.editor.vim import VimState
|
||||
from novelwriter.enum import nwVimMode
|
||||
from novelwriter.guimain import GuiMain
|
||||
from novelwriter.types import QtKeyEscape
|
||||
from novelwriter.types import QtKeyEscape, QtKeyRight, QtModShift
|
||||
|
||||
from tests.helpers import C, MockRnd, buildTestProject
|
||||
|
||||
@@ -98,13 +98,18 @@ def testEditorVimMode_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd
|
||||
assert vimMode._vim.mode == nwVimMode.INSERT
|
||||
assert docFooter._vimMode == nwVimMode.INSERT
|
||||
|
||||
# Escape returns to NORMAL mode first, then leaves Focus Mode
|
||||
# Escape returns to NORMAL mode first, then cancels a pending
|
||||
# command, and then leaves Focus Mode
|
||||
nwGUI.toggleFocusMode()
|
||||
assert SHARED.focusMode is True
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
assert vimMode.mode == nwVimMode.NORMAL
|
||||
assert vimMode._vim.mode == nwVimMode.NORMAL
|
||||
assert docFooter._vimMode == nwVimMode.NORMAL
|
||||
assert SHARED.focusMode is True
|
||||
qtbot.keyClick(docEditor, "d", delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
assert vimMode._vim.command == ""
|
||||
assert SHARED.focusMode is True
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
assert SHARED.focusMode is False
|
||||
|
||||
@@ -113,7 +118,7 @@ def testEditorVimMode_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd
|
||||
docEditor.setReadOnly(True)
|
||||
qtbot.keyClicks(docEditor, "xV", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == text
|
||||
assert vimMode.mode == nwVimMode.NORMAL
|
||||
assert vimMode._vim.mode == nwVimMode.NORMAL
|
||||
docEditor.setReadOnly(False)
|
||||
|
||||
# Unrelated settings changes don't reset the mode
|
||||
@@ -196,6 +201,18 @@ def testEditorVimMode_InsertMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
qtbot.keyClick(docEditor, QtKeyEscape)
|
||||
assert "above" in docEditor.getText().splitlines()[0]
|
||||
|
||||
# A/I: Append and insert at the ends of a wrapped paragraph
|
||||
para = " ".join(["word"] * 500)
|
||||
docEditor.setPlainText(para)
|
||||
docEditor.setCursorPosition(5)
|
||||
qtbot.keyClick(docEditor, "A", delay=KEY_DELAY)
|
||||
qtbot.keyClicks(docEditor, "X", delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, QtKeyEscape)
|
||||
qtbot.keyClick(docEditor, "I", delay=KEY_DELAY)
|
||||
qtbot.keyClicks(docEditor, "Y", delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, QtKeyEscape)
|
||||
assert docEditor.getText() == f"Y{para}X"
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testEditorVimMode_NormalMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd: MockRnd):
|
||||
@@ -251,6 +268,35 @@ def testEditorVimMode_NormalMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
qtbot.keyClick(docEditor, "l", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == 1
|
||||
|
||||
# h: Move left
|
||||
resetText(docEditor)
|
||||
docEditor.setCursorPosition(2)
|
||||
qtbot.keyClick(docEditor, "h", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == 1
|
||||
|
||||
# h/l: Don't move past the start or end of the line
|
||||
text = resetText(docEditor)
|
||||
docEditor.setCursorPosition(text.find("Line2"))
|
||||
qtbot.keyClick(docEditor, "h", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == text.find("Line2")
|
||||
docEditor.setCursorPosition(len("Line1"))
|
||||
qtbot.keyClick(docEditor, "l", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == len("Line1")
|
||||
|
||||
# Navigation keys move the cursor and cancel a pending command
|
||||
text = resetText(docEditor)
|
||||
docEditor.setCursorPosition(0)
|
||||
qtbot.keyClick(docEditor, "d", delay=KEY_DELAY)
|
||||
qtbot.keyClick(docEditor, QtKeyRight, delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == 1
|
||||
qtbot.keyClick(docEditor, "d", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == text
|
||||
|
||||
# Navigation keys with Shift are ignored
|
||||
qtbot.keyClick(docEditor, QtKeyRight, QtModShift, delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == 1
|
||||
assert docEditor.textCursor().hasSelection() is False
|
||||
|
||||
# 0: Move to beginning of line
|
||||
resetText(docEditor)
|
||||
docEditor.setCursorPosition(3)
|
||||
@@ -280,6 +326,15 @@ def testEditorVimMode_NormalMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
qtbot.keyClick(docEditor, "G", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == len(text)
|
||||
|
||||
# $/0: Move to the end and start of a wrapped paragraph
|
||||
para = " ".join(["word"] * 500)
|
||||
docEditor.setPlainText(para)
|
||||
docEditor.setCursorPosition(5)
|
||||
qtbot.keyClick(docEditor, "$", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == len(para)
|
||||
qtbot.keyClick(docEditor, "0", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == 0
|
||||
|
||||
# Unknown keys are discarded
|
||||
resetText(docEditor)
|
||||
docEditor.setCursorPosition(0)
|
||||
@@ -311,6 +366,7 @@ def testEditorVimMode_Delete(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
|
||||
docEditor.setCursorPosition(text.find("Line3"))
|
||||
qtbot.keyClicks(docEditor, "dd", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == "Line1\nLine2"
|
||||
assert docEditor.textCursor().position() == text.find("Line2")
|
||||
|
||||
# dd: Delete a wrapped paragraph
|
||||
text = " ".join(["word"] * 500)
|
||||
@@ -332,11 +388,23 @@ def testEditorVimMode_Delete(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
|
||||
qtbot.keyClicks(docEditor, "x", delay=KEY_DELAY)
|
||||
assert getLines(docEditor)[0] == "ine1"
|
||||
|
||||
# x: Don't delete the line break at the end of the line
|
||||
text = resetText(docEditor)
|
||||
docEditor.setCursorPosition(len("Line1"))
|
||||
qtbot.keyClicks(docEditor, "x", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == text
|
||||
|
||||
# dw: Delete word
|
||||
resetText(docEditor)
|
||||
docEditor.setCursorPosition(0)
|
||||
qtbot.keyClicks(docEditor, "dw", delay=KEY_DELAY)
|
||||
assert getLines(docEditor) == ["Line2", "Line3"]
|
||||
assert docEditor.getText() == "\nLine2\nLine3"
|
||||
|
||||
# dw: Don't delete the line break at the end of the line
|
||||
text = resetText(docEditor)
|
||||
docEditor.setCursorPosition(len("Line1"))
|
||||
qtbot.keyClicks(docEditor, "dw", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == text
|
||||
|
||||
# d$: Delete to end of line
|
||||
resetText(docEditor)
|
||||
@@ -401,19 +469,44 @@ def testEditorVimMode_YankPaste(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mo
|
||||
qtbot.keyClicks(docEditor, "P", delay=KEY_DELAY)
|
||||
assert getLines(docEditor) == ["Line1", "Line3", "Line2", "Line3"]
|
||||
|
||||
# yw + p: Yank word and paste
|
||||
# yy: Yank a wrapped paragraph without moving the cursor
|
||||
para = " ".join(["word"] * 500)
|
||||
docEditor.setPlainText(f"{para}\nLine2")
|
||||
docEditor.setCursorPosition(5)
|
||||
qtbot.keyClicks(docEditor, "yy", delay=KEY_DELAY)
|
||||
assert vimMode._vim.pasteFromInternal() == para
|
||||
assert vimMode._vim.lineWise is True
|
||||
assert docEditor.textCursor().position() == 5
|
||||
|
||||
# yw + p: Paste word after the cursor
|
||||
text = resetText(docEditor)
|
||||
docEditor.setCursorPosition(0)
|
||||
qtbot.keyClicks(docEditor, "yw", delay=KEY_DELAY)
|
||||
docEditor.setCursorPosition(text.find("Line2"))
|
||||
qtbot.keyClicks(docEditor, "p", delay=KEY_DELAY)
|
||||
assert getLines(docEditor) == ["Line1", "Line2", "Line1", "Line3"]
|
||||
assert docEditor.getText() == "Line1\nLLine1ine2\nLine3"
|
||||
|
||||
# x + p: Paste character at the end of a line
|
||||
resetText(docEditor)
|
||||
docEditor.setCursorPosition(0)
|
||||
qtbot.keyClicks(docEditor, "x", delay=KEY_DELAY)
|
||||
docEditor.setCursorPosition(len("ine1"))
|
||||
qtbot.keyClicks(docEditor, "p", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == "ine1L\nLine2\nLine3"
|
||||
|
||||
# x + P: Paste character before the cursor
|
||||
resetText(docEditor)
|
||||
docEditor.setCursorPosition(0)
|
||||
qtbot.keyClicks(docEditor, "xP", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == "Line1\nLine2\nLine3"
|
||||
|
||||
|
||||
@pytest.mark.gui
|
||||
def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd: MockRnd):
|
||||
"""Test vim VISUAL and V-LINE mode selection, yank and paste."""
|
||||
docEditor = openVimEditor(nwGUI, projPath)
|
||||
vimMode = docEditor._vimMode
|
||||
assert vimMode is not None
|
||||
originalText = resetText(docEditor)
|
||||
|
||||
# v: Select, yank and paste
|
||||
@@ -464,6 +557,7 @@ def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
assert docEditor.textCursor().position() == position
|
||||
|
||||
# b: Extend selection to start of word
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
text = resetText(docEditor)
|
||||
docEditor.setCursorPosition(text.find("Line2") + 2)
|
||||
qtbot.keyClick(docEditor, "v", delay=KEY_DELAY)
|
||||
@@ -471,6 +565,7 @@ def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
assert docEditor.textCursor().selectedText() == "Li"
|
||||
|
||||
# e: Extend selection to end of word
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
resetText(docEditor)
|
||||
docEditor.setCursorPosition(0)
|
||||
qtbot.keyClick(docEditor, "v", delay=KEY_DELAY)
|
||||
@@ -478,6 +573,7 @@ def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
assert docEditor.textCursor().selectedText() == "Line1"
|
||||
|
||||
# e: Extend selection to end of next word
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
docEditor.setPlainText("Line1 lineExtra Line2 Line3")
|
||||
docEditor.setCursorPosition(len("Line1") - 1)
|
||||
qtbot.keyClick(docEditor, "v", delay=KEY_DELAY)
|
||||
@@ -485,6 +581,7 @@ def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
assert "lineExtra" in docEditor.textCursor().selectedText()
|
||||
|
||||
# e: At the end of the document, there's nowhere further to extend
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
docEditor.setPlainText("Line1")
|
||||
docEditor.setCursorPosition(5)
|
||||
qtbot.keyClick(docEditor, "v", delay=KEY_DELAY)
|
||||
@@ -492,6 +589,7 @@ def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
assert docEditor.textCursor().position() == 5
|
||||
|
||||
# ggvG: Select all, delete and undo
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
resetText(docEditor)
|
||||
qtbot.keyClicks(docEditor, "g", delay=KEY_DELAY)
|
||||
qtbot.keyClicks(docEditor, "g", delay=KEY_DELAY)
|
||||
@@ -562,3 +660,61 @@ def testEditorVimMode_VisualMode(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, m
|
||||
qtbot.keyClick(docEditor, "G", delay=KEY_DELAY)
|
||||
qtbot.keyClicks(docEditor, "qgg", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == 0
|
||||
|
||||
# v: Leave VISUAL mode
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
text = resetText(docEditor)
|
||||
line2 = text.find("Line2")
|
||||
docEditor.setCursorPosition(line2 + 2)
|
||||
qtbot.keyClicks(docEditor, "vv", delay=KEY_DELAY)
|
||||
assert vimMode._vim.mode == nwVimMode.NORMAL
|
||||
assert docEditor.textCursor().hasSelection() is False
|
||||
|
||||
# V: Switch from VISUAL to V-LINE mode
|
||||
qtbot.keyClicks(docEditor, "vV", delay=KEY_DELAY)
|
||||
assert vimMode._vim.mode == nwVimMode.V_LINE
|
||||
assert docEditor.textCursor().selectedText() == "Line2"
|
||||
|
||||
# v: Switch from V-LINE to VISUAL mode
|
||||
qtbot.keyClick(docEditor, "v", delay=KEY_DELAY)
|
||||
assert vimMode._vim.mode == nwVimMode.VISUAL
|
||||
assert docEditor.textCursor().selectedText() == "Line2"
|
||||
|
||||
# V: Leave V-LINE mode
|
||||
qtbot.keyClicks(docEditor, "VV", delay=KEY_DELAY)
|
||||
assert vimMode._vim.mode == nwVimMode.NORMAL
|
||||
assert docEditor.textCursor().hasSelection() is False
|
||||
|
||||
# V: Keep the direction of a backwards selection
|
||||
docEditor.setCursorPosition(text.find("Line3") + 2)
|
||||
qtbot.keyClicks(docEditor, "vkV", delay=KEY_DELAY)
|
||||
cursor = docEditor.textCursor()
|
||||
assert vimMode._vim.mode == nwVimMode.V_LINE
|
||||
assert cursor.position() == line2
|
||||
assert cursor.anchor() == len(text)
|
||||
|
||||
# i: Not a visual command, so nothing happens
|
||||
qtbot.keyClick(docEditor, "i", delay=KEY_DELAY)
|
||||
assert vimMode._vim.mode == nwVimMode.V_LINE
|
||||
assert docEditor.getText() == text
|
||||
|
||||
# V: Select and delete whole wrapped paragraphs
|
||||
qtbot.keyClick(docEditor, QtKeyEscape, delay=KEY_DELAY)
|
||||
para = " ".join(["word"] * 500)
|
||||
docEditor.setPlainText(f"{para}\nLine2\nLine3")
|
||||
docEditor.setCursorPosition(5)
|
||||
qtbot.keyClick(docEditor, "V", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().selectedText() == para
|
||||
qtbot.keyClick(docEditor, "j", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == len(f"{para}\nLine2")
|
||||
qtbot.keyClick(docEditor, "d", delay=KEY_DELAY)
|
||||
assert docEditor.getText() == "Line3"
|
||||
assert vimMode._vim.lineWise is True
|
||||
|
||||
# h/l: Don't extend past the start or end of the line
|
||||
text = resetText(docEditor)
|
||||
docEditor.setCursorPosition(text.find("Line2"))
|
||||
qtbot.keyClicks(docEditor, "vhh", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == text.find("Line2")
|
||||
qtbot.keyClicks(docEditor, "$l", delay=KEY_DELAY)
|
||||
assert docEditor.textCursor().position() == text.find("Line3") - 1
|
||||
|
||||
Reference in New Issue
Block a user