diff --git a/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt b/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt index a1e724308..e29efbcea 100644 --- a/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt +++ b/src/main/java/com/maddyhome/idea/vim/extension/nerdtree/NerdTree.kt @@ -555,12 +555,13 @@ private fun registerCommand(default: String, action: NerdAction) { } -private val actionsRoot: RootNode<NerdAction> = RootNode() +private val actionsRoot: RootNode<NerdAction> = RootNode("NERDTree") private var currentNode: CommandPartNode<NerdAction> = actionsRoot + private fun collectShortcuts(node: Node<NerdAction>): Set<KeyStroke> { return if (node is CommandPartNode<NerdAction>) { - val res = node.keys.toMutableSet() - res += node.values.map { collectShortcuts(it) }.flatten() + val res = node.children.keys.toMutableSet() + res += node.children.values.map { collectShortcuts(it) }.flatten() res } else { emptySet() diff --git a/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt b/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt index 68d6c1c92..de661704f 100644 --- a/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt +++ b/tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased/RandomActionsPropertyTest.kt @@ -95,7 +95,7 @@ private class AvailableActions(private val editor: Editor) : ImperativeCommand { val currentNode = KeyHandler.getInstance().keyHandlerState.commandBuilder.getCurrentTrie() // Note: esc is always an option - val possibleKeys = (currentNode.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) } + val possibleKeys = (currentNode.children.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) } println("Keys: ${possibleKeys.joinToString(", ")}") val keyGenerator = Generator.integers(0, possibleKeys.lastIndex) .suchThat { injector.parser.toKeyNotation(possibleKeys[it]) !in stinkyKeysList } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt index f78174417..59a7cab5c 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/KeyHandler.kt @@ -22,9 +22,9 @@ import com.maddyhome.idea.vim.diagnostic.VimLogger import com.maddyhome.idea.vim.diagnostic.trace import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.impl.state.toMappingMode -import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.key.KeyConsumer import com.maddyhome.idea.vim.key.KeyStack +import com.maddyhome.idea.vim.key.RootNode import com.maddyhome.idea.vim.key.consumers.CharArgumentConsumer import com.maddyhome.idea.vim.key.consumers.CommandConsumer import com.maddyhome.idea.vim.key.consumers.CommandCountConsumer @@ -282,7 +282,7 @@ class KeyHandler { keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode())) } - private fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> { + private fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> { return injector.keyGroup.getKeyRoot(mappingMode) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt index db9e0c8c3..5733bbc45 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroup.kt @@ -10,17 +10,17 @@ package com.maddyhome.idea.vim.api import com.maddyhome.idea.vim.action.change.LazyVimCommand import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.extension.ExtensionHandler -import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.key.KeyMapping import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.MappingInfo import com.maddyhome.idea.vim.key.MappingOwner +import com.maddyhome.idea.vim.key.RootNode import com.maddyhome.idea.vim.key.ShortcutOwnerInfo import com.maddyhome.idea.vim.vimscript.model.expressions.Expression import javax.swing.KeyStroke interface VimKeyGroup { - fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> + fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer fun getActions(editor: VimEditor, keyStroke: KeyStroke): List<NativeAction> fun getKeymapConflicts(keyStroke: KeyStroke): List<NativeAction> diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt index 1d7a7b3d4..5395e9c83 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/api/VimKeyGroupBase.kt @@ -12,7 +12,6 @@ import com.maddyhome.idea.vim.action.change.LazyVimCommand import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.handler.EditorActionHandlerBase -import com.maddyhome.idea.vim.key.CommandPartNode import com.maddyhome.idea.vim.key.KeyMapping import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.MappingInfo @@ -30,7 +29,7 @@ abstract class VimKeyGroupBase : VimKeyGroup { @JvmField val myShortcutConflicts: MutableMap<KeyStroke, ShortcutOwnerInfo> = LinkedHashMap() val requiredShortcutKeys: MutableSet<RequiredShortcut> = HashSet(300) - val keyRoots: MutableMap<MappingMode, CommandPartNode<LazyVimCommand>> = EnumMap(MappingMode::class.java) + val keyRoots: MutableMap<MappingMode, RootNode<LazyVimCommand>> = EnumMap(MappingMode::class.java) val keyMappings: MutableMap<MappingMode, KeyMapping> = EnumMap(MappingMode::class.java) override fun removeKeyMapping(modes: Set<MappingMode>, keys: List<KeyStroke>) { @@ -63,7 +62,7 @@ abstract class VimKeyGroupBase : VimKeyGroup { * @param mappingMode The mapping mode * @return The key mapping tree root */ - override fun getKeyRoot(mappingMode: MappingMode): CommandPartNode<LazyVimCommand> = keyRoots.getOrPut(mappingMode) { RootNode() } + override fun getKeyRoot(mappingMode: MappingMode): RootNode<LazyVimCommand> = keyRoots.getOrPut(mappingMode) { RootNode(mappingMode.name.get(0).lowercase()) } override fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer = getKeyMapping(mode) 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 8794c8272..5f9be2d8c 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 @@ -32,14 +32,8 @@ class CommandBuilder private constructor( private val keyList: MutableList<KeyStroke>, ) : Cloneable { - constructor( - currentCommandPartNode: CommandPartNode<LazyVimCommand>, - initialUncommittedRawCount: Int = 0 - ) : this( - currentCommandPartNode, - mutableListOf(initialUncommittedRawCount), - mutableListOf(), - ) + constructor(rootNode: RootNode<LazyVimCommand>, initialUncommittedRawCount: Int = 0) + : this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf()) private var selectedRegister: Char? = null private var action: EditorActionHandlerBase? = null @@ -263,13 +257,13 @@ class CommandBuilder private constructor( val node = currentCommandPartNode[key] when (node) { is CommandNode -> { - logger.trace { "Found full command node - $node ($key)" } + logger.trace { "Found full command node ($key) - ${node.debugString}" } addKey(key) processor(node.actionHolder.instance) return true } is CommandPartNode -> { - logger.trace { "Found command part node - $node ($key)" } + logger.trace { "Found command part node ($key) - ${node.debugString}" } currentCommandPartNode = node addKey(key) return true @@ -290,7 +284,7 @@ class CommandBuilder private constructor( * @see DuplicableOperatorAction */ fun convertDuplicateOperatorKeyStrokeToMotion(key: KeyStroke): KeyStroke { - logger.trace("convertDuplicateOperatorKeyStrokeToMotion is executed. key = $key") + logger.trace { "convertDuplicateOperatorKeyStrokeToMotion is executed. key = $key" } // Simple check to ensure that we're in OP_PENDING. If we don't have an action, we don't have an operator. If we // have an argument, we can't be in OP_PENDING @@ -319,13 +313,13 @@ class CommandBuilder private constructor( fun buildCommand(): Command { val rawCount = calculateCount0Snapshot() val command = Command(selectedRegister, rawCount, action!!, argument, action!!.type, action?.flags ?: noneOfEnum()) - resetAll(currentCommandPartNode) + resetAll(currentCommandPartNode.root as RootNode<LazyVimCommand>) return command } - fun resetAll(commandPartNode: CommandPartNode<LazyVimCommand>) { + fun resetAll(rootNode: RootNode<LazyVimCommand>) { logger.trace("resetAll is executed") - resetInProgressCommandPart(commandPartNode) + currentCommandPartNode = rootNode commandState = CurrentCommandState.NEW_COMMAND counts.clear() counts.add(0) @@ -337,12 +331,16 @@ class CommandBuilder private constructor( fallbackArgumentType = null } - // TODO: Why do we need this to be public? - // Who needs to reset the current command part node without resetting the whole command builder? - fun resetInProgressCommandPart(commandPartNode: CommandPartNode<LazyVimCommand>) { - logger.trace("resetInProgressCommandPart is executed") - counts[counts.size - 1] = 0 - currentCommandPartNode = commandPartNode + /** + * Change the command trie root node used to find commands for the current mode + * + * Typically, we reset the command trie root node after a command is executed, using the root node of the current + * mode - this is handled by [resetAll]. This function allows us to change the root node without executing a command + * or fully resetting the command builder, such as when switching to Op-pending while entering an operator+motion. + */ + fun resetCommandTrieRootNode(rootNode: RootNode<LazyVimCommand>) { + logger.trace("resetCommandTrieRootNode is executed") + currentCommandPartNode = rootNode } @TestOnly 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 16a64f3ad..1c01bf243 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 @@ -40,17 +40,36 @@ import javax.swing.KeyStroke * and the user should complete the sequence, it's [CommandPartNode] */ @Suppress("GrazieInspection") -interface Node<T> +interface Node<T> { + val debugString: String + val parent: Node<T>? + + val root: Node<T> + get() = parent?.root ?: this +} /** Represents a complete command */ -data class CommandNode<T>(val actionHolder: T) : Node<T> { - override fun toString(): String { - return "COMMAND NODE (${ actionHolder.toString() })" - } +data class CommandNode<T>(override val parent: Node<T>, val actionHolder: T, private val name: String) : Node<T> { + override val debugString: String + get() = toString() + + override fun toString() = "COMMAND NODE ($name - ${actionHolder.toString()})" } /** Represents a part of the command */ -open class CommandPartNode<T> : Node<T>, HashMap<KeyStroke, Node<T>>() { +open class CommandPartNode<T>( + override val parent: Node<T>?, + internal val name: String, + internal val depth: Int) : Node<T> { + + val children = mutableMapOf<KeyStroke, Node<T>>() + + operator fun set(stroke: KeyStroke, node: Node<T>) { + children[stroke] = node + } + + operator fun get(stroke: KeyStroke): Node<T>? = children[stroke] + override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false @@ -58,21 +77,32 @@ open class CommandPartNode<T> : Node<T>, HashMap<KeyStroke, Node<T>>() { return true } - override fun hashCode(): Int { - return super.hashCode() - } + override fun hashCode() = super.hashCode() - override fun toString(): String { - return """ - COMMAND PART NODE( - ${entries.joinToString(separator = "\n") { " " + injector.parser.toKeyNotation(it.key) + " - " + it.value }} - ) - """.trimIndent() - } + override fun toString() = "COMMAND PART NODE ($name - ${children.size} children)" + + override val debugString + get() = buildString { + append("COMMAND PART NODE(") + appendLine(name) + children.entries.forEach { + repeat(depth + 1) { append(" ") } + append(injector.parser.toKeyNotation(it.key)) + append(" - ") + appendLine(it.value.debugString) + } + repeat(depth) { append(" ") } + append(")") + } } /** Represents a root node for the mode */ -class RootNode<T> : CommandPartNode<T>() +class RootNode<T>(name: String) : CommandPartNode<T>(null, name, 0) { + override val debugString: String + get() = "ROOT NODE ($name)\n" + super.debugString + + override fun toString() = "ROOT NODE ($name - ${children.size} children)" +} fun <T> Node<T>.addLeafs(keyStrokes: List<KeyStroke>, actionHolder: T) { var node: Node<T> = this @@ -90,7 +120,16 @@ private fun <T> addNode(base: CommandPartNode<T>, actionHolder: T, key: KeyStrok val existing = base[key] if (existing != null) return existing - val newNode: Node<T> = if (isLastInSequence) CommandNode(actionHolder) else CommandPartNode() + val childName = injector.parser.toKeyNotation(key) + val name = when (base) { + is RootNode -> base.name + "_" + childName + else -> base.name + childName + } + val newNode: Node<T> = if (isLastInSequence) { + CommandNode(base, actionHolder, name) + } else { + CommandPartNode(base, name, base.depth + 1) + } base[key] = newNode return newNode } 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 55f15d4f7..41b2c0a18 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 @@ -68,7 +68,7 @@ data class KeyHandlerState( fun partialReset(mode: Mode) { logger.trace("entered partialReset. mode: $mode") mappingState.resetMappingSequence() - commandBuilder.resetInProgressCommandPart(injector.keyGroup.getKeyRoot(mode.toMappingMode())) + commandBuilder.resetCommandTrieRootNode(injector.keyGroup.getKeyRoot(mode.toMappingMode())) } fun reset(mode: Mode) {