Complete the connection between editor and vim handler

This commit is contained in:
Veronica Berglyd Olsen
2026-11-03 19:15:56 +01:00
parent b8f14bfb5d
commit 3355b5edbe
2 changed files with 58 additions and 39 deletions

View File

@@ -89,7 +89,7 @@ from novelwriter.editor.highlighter import BLOCK_META, BLOCK_TITLE
from novelwriter.editor.hovercard import GuiDocHoverCard
from novelwriter.editor.runnables import T_TextCheckPayload, TextCheckDispatcher, WordCounterDispatcher
from novelwriter.editor.textblock import T_TextCheckList, TextBlockData
from novelwriter.editor.vim import DocumentVimMode
from novelwriter.editor.vim import EditorVimMode
from novelwriter.enum import (
nwChange,
nwComment,
@@ -462,8 +462,8 @@ class GuiDocEditor(QTextEdit):
self.searchVisible = self.docSearch.isVisible
self.changeFocusState = self.docHeader.changeFocusState
# Vim State for vimMode
self._vimMode = DocumentVimMode(self)
# Vim Mode
self._vimMode: EditorVimMode | None = None
# Finalise
self.updateSyntaxColors()
@@ -652,8 +652,7 @@ class GuiDocEditor(QTextEdit):
else:
self.clearEditor()
# Refresh Vim Mode
self._vimMode.setVimMode(nwVimMode.NORMAL)
self._initVimMode()
def initViewport(self) -> None:
"""Initialise the settings of the editor viewport."""
@@ -683,7 +682,7 @@ class GuiDocEditor(QTextEdit):
self.docHeader.setHandle(self._docHandle)
self.docSearch.setAutoReplaceEnabled(CONFIG.doReplace)
if updateVimMode:
self._vimMode.setVimMode(nwVimMode.NORMAL)
self._initVimMode()
def loadText(self, tHandle: str, tLine: int | None = None) -> bool:
"""Load text from a document into the editor. If we have an I/O
@@ -743,6 +742,8 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(False)
self._qDocument.clearUndoRedoStacks()
self.docToolBar.setVisible(CONFIG.showEditToolBar)
if self._vimMode:
self._vimMode.setVimMode(nwVimMode.NORMAL)
# Process State Changes
SHARED.project.data.setLastHandle(tHandle, "editor")
@@ -1248,14 +1249,14 @@ class GuiDocEditor(QTextEdit):
if key == QtKeyEscape:
if self.searchVisible():
self.closeSearch()
elif CONFIG.vimMode:
elif self._vimMode:
self._vimMode.setVimMode(nwVimMode.NORMAL)
elif SHARED.focusMode:
SHARED.setFocusMode(False)
event.ignore()
return
if CONFIG.vimMode and self._vimMode.processVimKeyEvent(event):
if self._vimMode and self._vimMode.processVimKeyEvent(event):
return
if self.docSearch.anyFocus() and key in ENTER_KEYS:
@@ -2732,6 +2733,16 @@ class GuiDocEditor(QTextEdit):
options.setFlags(options.flags() | QTextOption.Flag.ShowLineAndParagraphSeparators)
self._qDocument.setDefaultTextOption(options)
def _initVimMode(self) -> None:
"""Enable or disable vim mode from the user settings."""
if CONFIG.vimMode:
if self._vimMode is None:
self._vimMode = EditorVimMode(self, self.docFooter.updateVimModeStatusBar)
self._vimMode.setVimMode(nwVimMode.NORMAL)
else:
self._vimMode = None
self.docFooter.updateVimModeStatusBar(None)
def _dispatchKeyPress(self, event: QKeyEvent) -> None:
"""Send a key event to the base class, except plain Return.

View File

@@ -1,6 +1,6 @@
"""
novelWriter - GUI Document Vim Mode
===================================
novelWriter - GUI Editor Vim Mode
=================================
This file is a part of novelWriter
Copyright (C) 2026 Veronica Berglyd Olsen and novelWriter contributors
@@ -25,7 +25,6 @@ import logging
from typing import TYPE_CHECKING
from novelwriter import CONFIG
from novelwriter.enum import nwDocAction, nwVimMode
from novelwriter.types import (
QtKeepAnchor,
@@ -45,6 +44,8 @@ from novelwriter.types import (
)
if TYPE_CHECKING:
from collections.abc import Callable
from PyQt6.QtGui import QKeyEvent, QTextCursor
from novelwriter.editor.editor import GuiDocEditor
@@ -52,32 +53,38 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
class DocumentVimMode:
"""Gui Widget: Editor Vim Mode."""
class EditorVimMode:
"""Editor Component: Vim Mode."""
__slots__ = ("_editor", "_vim")
__slots__ = ("_callback", "_editor", "_vim")
def __init__(self, editor: GuiDocEditor) -> None:
def __init__(self, editor: GuiDocEditor, callback: Callable[[nwVimMode], None]) -> None:
self._editor = editor
self._vim = VimState()
self._callback = callback
##
# Setters
##
def setVimMode(self, mode: nwVimMode) -> None:
"""Change the vim mode."""
if CONFIG.vimMode:
if mode == nwVimMode.NORMAL:
cursor = self.textCursor()
cursor.clearSelection()
self.setTextCursor(cursor)
self._vim.setMode(mode)
self._editor.docFooter.updateVimModeStatusBar(mode)
else:
self._editor.docFooter.updateVimModeStatusBar(None)
if mode == nwVimMode.NORMAL:
cursor = self.textCursor()
cursor.clearSelection()
self.setTextCursor(cursor)
self._vim.setMode(mode)
self._callback(mode)
def setTextCursor(self, cursor: QTextCursor) -> None:
"""Set the text cursor."""
self._editor.setTextCursor(cursor)
##
# Methods
##
def textCursor(self) -> QTextCursor:
"""Return a new text cursor."""
return self._editor.textCursor()
@@ -207,6 +214,7 @@ class DocumentVimMode:
if origPos < textLen:
cursor.movePosition(QtMoveNextChar, QtKeepAnchor)
cursor.movePosition(QtMoveEndOfWord, QtKeepAnchor)
self._vim.yankToInternal(cursor.selectedText())
cursor.removeSelectedText()
cursor.endEditBlock()
@@ -418,7 +426,7 @@ class DocumentVimMode:
class VimState:
"""Minimal Vim state machine."""
__slots__ = ("_internalClipboard", "_mode", "_normalCommand", "_visualCommand")
__slots__ = ("_buffer", "_mode", "_normalCmd", "_visualCmd")
PREFIX_KEYS = "dygz"
VISUAL_PREFIX_KEYS = "g"
@@ -426,9 +434,9 @@ class VimState:
def __init__(self) -> None:
self._mode: nwVimMode = nwVimMode.NORMAL
self._normalCommand = ""
self._visualCommand = ""
self._internalClipboard = ""
self._buffer = ""
self._normalCmd = ""
self._visualCmd = ""
self.setMode(nwVimMode.NORMAL)
@property
@@ -440,9 +448,9 @@ class VimState:
def command(self) -> str:
"""Return the current vim command."""
if self._mode in (nwVimMode.VISUAL, nwVimMode.V_LINE):
return self._visualCommand
return self._visualCmd
else:
return self._normalCommand
return self._normalCmd
def setMode(self, mode: nwVimMode) -> None:
"""Switch vim mode."""
@@ -453,27 +461,27 @@ class VimState:
def resetCommand(self) -> None:
"""Reset internal vim command."""
self._normalCommand = ""
self._visualCommand = ""
self._normalCmd = ""
self._visualCmd = ""
def pushCommandKey(self, key: str) -> None:
"""Push key to the current command building stack."""
if self._mode is nwVimMode.NORMAL:
self._normalCommand += key
self._normalCmd += key
elif self._mode in (nwVimMode.VISUAL, nwVimMode.V_LINE):
self._visualCommand += key
self._visualCmd += key
def setCommand(self, key: str) -> None:
"""Set the state of the current vim command."""
if self._mode is nwVimMode.NORMAL:
self._normalCommand = key
self._normalCmd = key
elif self._mode in (nwVimMode.VISUAL, nwVimMode.V_LINE):
self._visualCommand = key
self._visualCmd = key
def yankToInternal(self, text: str) -> None:
"""Put text into internal vim buffer."""
self._internalClipboard = text
self._buffer = text
def pasteFromInternal(self) -> str:
"""Paste from the internal vim clipboard."""
return self._internalClipboard
"""Paste from the internal vim buffer."""
return self._buffer