Fix various behaviour inconsistencies

This commit is contained in:
Veronica Berglyd Olsen
2026-11-03 20:13:10 +01:00
parent b5c8a014b7
commit 037f1bee08
4 changed files with 293 additions and 67 deletions

View File

@@ -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()

View File

@@ -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."""

View File

@@ -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

View File

@@ -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