diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/LazyVimCommand.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/LazyVimCommand.kt
index a61d976f6..f3119d5f5 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/LazyVimCommand.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/action/change/LazyVimCommand.kt
@@ -20,4 +20,24 @@ public class LazyVimCommand(
   classLoader: ClassLoader,
 ) : LazyInstance<EditorActionHandlerBase>(className, classLoader) {
   public val actionId: String = EditorActionHandlerBase.getActionId(className)
+
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as LazyVimCommand
+
+    if (keys != other.keys) return false
+    if (modes != other.modes) return false
+    if (actionId != other.actionId) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = keys.hashCode()
+    result = 31 * result + modes.hashCode()
+    result = 31 * result + actionId.hashCode()
+    return result
+  }
 }
\ No newline at end of file
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt
index 317452d40..6a79f2b42 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/CommandBuilder.kt
@@ -191,6 +191,33 @@ public class CommandBuilder(private var currentCommandPartNode: CommandPartNode<
 
   @TestOnly
   public fun getCurrentTrie(): CommandPartNode<LazyVimCommand> = currentCommandPartNode
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as CommandBuilder
+
+    if (currentCommandPartNode != other.currentCommandPartNode) return false
+    if (commandParts != other.commandParts) return false
+    if (keyList != other.keyList) return false
+    if (commandState != other.commandState) return false
+    if (count != other.count) return false
+    if (expectedArgumentType != other.expectedArgumentType) return false
+    if (prevExpectedArgumentType != other.prevExpectedArgumentType) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = currentCommandPartNode.hashCode()
+    result = 31 * result + commandParts.hashCode()
+    result = 31 * result + keyList.hashCode()
+    result = 31 * result + commandState.hashCode()
+    result = 31 * result + count
+    result = 31 * result + (expectedArgumentType?.hashCode() ?: 0)
+    result = 31 * result + (prevExpectedArgumentType?.hashCode() ?: 0)
+    return result
+  }
 
   public companion object {
     private val LOG = vimLogger<CommandBuilder>()
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingState.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingState.kt
index 5c6488fbd..770778970 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingState.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingState.kt
@@ -35,7 +35,7 @@ public class MappingState {
   public val keys: Iterable<KeyStroke>
     get() = keyList
 
-  private val timer = Timer(injector.globalOptions().timeoutlen, null)
+  private val timer = VimTimer(injector.globalOptions().timeoutlen)
   private var keyList = mutableListOf<KeyStroke>()
 
   init {
@@ -72,7 +72,46 @@ public class MappingState {
     // NOTE: We intentionally don't reset mapping mode here
   }
 
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as MappingState
+
+    if (mapDepth != other.mapDepth) return false
+    if (timer != other.timer) return false
+    if (keyList != other.keyList) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = mapDepth
+    result = 31 * result + timer.hashCode()
+    result = 31 * result + keyList.hashCode()
+    return result
+  }
+
   public companion object {
     private val LOG = vimLogger<MappingState>()
   }
-}
+
+  public class VimTimer(delay: Int) : Timer(delay, null) {
+    override fun equals(other: Any?): Boolean {
+      if (this === other) return true
+      if (javaClass != other?.javaClass) return false
+
+      other as VimTimer
+
+      if (delay != other.delay) return false
+      if (initialDelay != other.initialDelay) return false
+      if (isRunning != other.isRunning) return false
+
+      return true
+    }
+
+    override fun hashCode(): Int {
+      return javaClass.hashCode()
+    }
+  }
+}
\ No newline at end of file
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/DigraphSequence.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/DigraphSequence.kt
index ca2e49e03..4fe4b9560 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/DigraphSequence.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/common/DigraphSequence.kt
@@ -222,6 +222,32 @@ public class DigraphSequence {
     codeChars = CharArray(8)
   }
 
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+
+    other as DigraphSequence
+
+    if (digraphState != other.digraphState) return false
+    if (digraphChar != other.digraphChar) return false
+    if (!codeChars.contentEquals(other.codeChars)) return false
+    if (codeCnt != other.codeCnt) return false
+    if (codeType != other.codeType) return false
+    if (codeMax != other.codeMax) return false
+
+    return true
+  }
+
+  override fun hashCode(): Int {
+    var result = digraphState
+    result = 31 * result + digraphChar.hashCode()
+    result = 31 * result + codeChars.contentHashCode()
+    result = 31 * result + codeCnt
+    result = 31 * result + codeType
+    result = 31 * result + codeMax
+    return result
+  }
+
   public companion object {
     private const val DIG_STATE_PENDING = 1
     private const val DIG_STATE_DIG_ONE = 2
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt
index 649fe3b61..8db5bfd10 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/Nodes.kt
@@ -42,10 +42,21 @@ import javax.swing.KeyStroke
 public interface Node<T>
 
 /** Represents a complete command */
-public class CommandNode<T>(public val actionHolder: T) : Node<T>
+public data class CommandNode<T>(public val actionHolder: T) : Node<T>
 
 /** Represents a part of the command */
-public open class CommandPartNode<T> : Node<T>, HashMap<KeyStroke, Node<T>>()
+public open class CommandPartNode<T> : Node<T>, HashMap<KeyStroke, Node<T>>() {
+  override fun equals(other: Any?): Boolean {
+    if (this === other) return true
+    if (javaClass != other?.javaClass) return false
+    if (!super.equals(other)) return false
+    return true
+  }
+
+  override fun hashCode(): Int {
+    return super.hashCode()
+  }
+}
 
 /** Represents a root node for the mode */
 public class RootNode<T> : CommandPartNode<T>()
diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt
index ab72f86ec..9f58e406d 100644
--- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt
+++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/state/KeyHandlerState.kt
@@ -16,10 +16,12 @@ import com.maddyhome.idea.vim.common.DigraphSequence
 import com.maddyhome.idea.vim.impl.state.toMappingMode
 import com.maddyhome.idea.vim.state.mode.Mode
 
-public class KeyHandlerState {
-  public val mappingState: MappingState = MappingState()
-  public val digraphSequence: DigraphSequence = DigraphSequence()
-  public val commandBuilder: CommandBuilder = CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.NORMAL))
+public data class KeyHandlerState(
+  public val mappingState: MappingState,
+  public val digraphSequence: DigraphSequence,
+  public val commandBuilder: CommandBuilder,
+) {
+  public constructor() : this(MappingState(), DigraphSequence(), CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.NORMAL)))
 
   public fun partialReset(mode: Mode) {
     digraphSequence.reset()