From 9afab26127a42d23747fa768162f432442c559a1 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Wed, 30 Oct 2024 14:02:34 +0000 Subject: [PATCH 01/13] Introduce KeyStrokeTrie to find commands Should also restore compatibility with idea-which-key --- .../idea/vim/extension/nerdtree/NerdTree.kt | 62 +++---- .../maddyhome/idea/vim/group/KeyGroup.java | 5 +- .../com/maddyhome/idea/vim/key/NodesHelper.kt | 15 -- .../RandomActionsPropertyTest.kt | 17 +- .../com/maddyhome/idea/vim/KeyHandler.kt | 11 +- .../com/maddyhome/idea/vim/api/VimKeyGroup.kt | 8 +- .../maddyhome/idea/vim/api/VimKeyGroupBase.kt | 18 +- .../idea/vim/command/CommandBuilder.kt | 98 +++++------ .../maddyhome/idea/vim/key/KeyStrokeTrie.kt | 156 ++++++++++++++++++ .../com/maddyhome/idea/vim/key/Nodes.kt | 119 +++++-------- .../idea/vim/key/consumers/DigraphConsumer.kt | 8 +- .../vim/key/consumers/RegisterConsumer.kt | 2 +- .../idea/vim/state/KeyHandlerState.kt | 8 +- 13 files changed, 315 insertions(+), 212 deletions(-) delete mode 100644 src/main/java/com/maddyhome/idea/vim/key/NodesHelper.kt create mode 100644 vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt 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 e29efbcea1..fccb03c803 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 @@ -42,13 +42,10 @@ import com.maddyhome.idea.vim.extension.VimExtension import com.maddyhome.idea.vim.group.KeyGroup import com.maddyhome.idea.vim.helper.MessageHelper import com.maddyhome.idea.vim.helper.runAfterGotFocus -import com.maddyhome.idea.vim.key.CommandNode -import com.maddyhome.idea.vim.key.CommandPartNode +import com.maddyhome.idea.vim.key.KeyStrokeTrie import com.maddyhome.idea.vim.key.MappingOwner -import com.maddyhome.idea.vim.key.Node import com.maddyhome.idea.vim.key.RequiredShortcut -import com.maddyhome.idea.vim.key.RootNode -import com.maddyhome.idea.vim.key.addLeafs +import com.maddyhome.idea.vim.key.add import com.maddyhome.idea.vim.newapi.ij import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.vimscript.model.datatypes.VimString @@ -198,6 +195,8 @@ internal class NerdTree : VimExtension { internal var waitForSearch = false internal var speedSearchListenerInstalled = false + private val keys = mutableListOf() + override fun actionPerformed(e: AnActionEvent) { var keyStroke = getKeyStroke(e) ?: return val keyChar = keyStroke.keyChar @@ -205,20 +204,14 @@ internal class NerdTree : VimExtension { keyStroke = KeyStroke.getKeyStroke(keyChar) } - val nextNode = currentNode[keyStroke] - - when (nextNode) { - null -> currentNode = actionsRoot - is CommandNode -> { - currentNode = actionsRoot - - val action = nextNode.actionHolder - when (action) { - is NerdAction.ToIj -> Util.callAction(null, action.name, e.dataContext.vim) - is NerdAction.Code -> e.project?.let { action.action(it, e.dataContext, e) } - } + keys.add(keyStroke) + actionsRoot.getData(keys)?.let { action -> + when (action) { + is NerdAction.ToIj -> Util.callAction(null, action.name, e.dataContext.vim) + is NerdAction.Code -> e.project?.let { action.action(it, e.dataContext, e) } } - is CommandPartNode -> currentNode = nextNode + + keys.clear() } } @@ -540,38 +533,29 @@ private fun addCommand(alias: String, handler: CommandAliasHandler) { VimPlugin.getCommand().setAlias(alias, CommandAlias.Call(0, -1, alias, handler)) } -private fun registerCommand(variable: String, default: String, action: NerdAction) { +private fun registerCommand(variable: String, defaultMapping: String, action: NerdAction) { val variableValue = VimPlugin.getVariableService().getGlobalVariableValue(variable) - val mappings = if (variableValue is VimString) { + val mapping = if (variableValue is VimString) { variableValue.value } else { - default + defaultMapping } - actionsRoot.addLeafs(mappings, action) + registerCommand(mapping, action) } -private fun registerCommand(default: String, action: NerdAction) { - actionsRoot.addLeafs(default, action) -} - - -private val actionsRoot: RootNode = RootNode("NERDTree") -private var currentNode: CommandPartNode = actionsRoot - -private fun collectShortcuts(node: Node): Set { - return if (node is CommandPartNode) { - val res = node.children.keys.toMutableSet() - res += node.children.values.map { collectShortcuts(it) }.flatten() - res - } else { - emptySet() +private fun registerCommand(mapping: String, action: NerdAction) { + actionsRoot.add(mapping, action) + injector.parser.parseKeys(mapping).forEach { + distinctShortcuts.add(it) } } +private val actionsRoot: KeyStrokeTrie = KeyStrokeTrie("NERDTree") +private val distinctShortcuts = mutableSetOf() + private fun installDispatcher(project: Project) { val dispatcher = NerdTree.NerdDispatcher.getInstance(project) - val shortcuts = - collectShortcuts(actionsRoot).map { RequiredShortcut(it, MappingOwner.Plugin.get(NerdTree.pluginName)) } + val shortcuts = distinctShortcuts.map { RequiredShortcut(it, MappingOwner.Plugin.get(NerdTree.pluginName)) } dispatcher.registerCustomShortcutSet( KeyGroup.toShortcutSet(shortcuts), (ProjectView.getInstance(project) as ProjectViewImpl).component, diff --git a/src/main/java/com/maddyhome/idea/vim/group/KeyGroup.java b/src/main/java/com/maddyhome/idea/vim/group/KeyGroup.java index e0aa81a1b5..004a8b1e38 100644 --- a/src/main/java/com/maddyhome/idea/vim/group/KeyGroup.java +++ b/src/main/java/com/maddyhome/idea/vim/group/KeyGroup.java @@ -18,7 +18,6 @@ import com.intellij.openapi.components.State; import com.intellij.openapi.components.Storage; import com.intellij.openapi.diagnostic.Logger; -import com.intellij.openapi.editor.Editor; import com.intellij.openapi.keymap.Keymap; import com.intellij.openapi.keymap.KeymapManager; import com.intellij.openapi.keymap.ex.KeymapManagerEx; @@ -28,7 +27,6 @@ import com.maddyhome.idea.vim.action.change.LazyVimCommand; import com.maddyhome.idea.vim.api.*; import com.maddyhome.idea.vim.command.MappingMode; -import com.maddyhome.idea.vim.ex.ExOutputModel; import com.maddyhome.idea.vim.key.*; import com.maddyhome.idea.vim.newapi.IjNativeAction; import com.maddyhome.idea.vim.newapi.IjVimEditor; @@ -199,8 +197,7 @@ public void registerCommandAction(@NotNull LazyVimCommand command) { registerRequiredShortcut(keyStrokes, MappingOwner.IdeaVim.System.INSTANCE); for (MappingMode mappingMode : command.getModes()) { - Node node = getKeyRoot(mappingMode); - NodesKt.addLeafs(node, keyStrokes, command); + getBuiltinCommandsTrie(mappingMode).add(keyStrokes, command); } } } diff --git a/src/main/java/com/maddyhome/idea/vim/key/NodesHelper.kt b/src/main/java/com/maddyhome/idea/vim/key/NodesHelper.kt deleted file mode 100644 index 70deaaa324..0000000000 --- a/src/main/java/com/maddyhome/idea/vim/key/NodesHelper.kt +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright 2003-2023 The IdeaVim authors - * - * Use of this source code is governed by an MIT-style - * license that can be found in the LICENSE.txt file or at - * https://opensource.org/licenses/MIT. - */ - -package com.maddyhome.idea.vim.key - -import com.maddyhome.idea.vim.api.injector - -internal fun Node.addLeafs(keys: String, actionHolder: T) { - addLeafs(injector.parser.parseKeys(keys), actionHolder) -} 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 dbd66c539d..01a75dd219 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 @@ -14,7 +14,6 @@ import com.intellij.testFramework.PlatformTestUtil import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.key -import com.maddyhome.idea.vim.key.CommandNode import com.maddyhome.idea.vim.newapi.vim import org.jetbrains.jetCheck.Generator import org.jetbrains.jetCheck.ImperativeCommand @@ -92,19 +91,23 @@ class RandomActionsPropertyTest : VimPropertyTestBase() { private class AvailableActions(private val editor: Editor) : ImperativeCommand { override fun performCommand(env: ImperativeCommand.Environment) { - val currentNode = KeyHandler.getInstance().keyHandlerState.commandBuilder.getCurrentTrie() + val trie = KeyHandler.getInstance().keyHandlerState.commandBuilder.getCurrentTrie() + val currentKeys = KeyHandler.getInstance().keyHandlerState.commandBuilder.getCurrentCommandKeys() // Note: esc is always an option - val possibleKeys = (currentNode.children.keys.toList() + esc).sortedBy { injector.parser.toKeyNotation(it) } - println("Keys: ${possibleKeys.joinToString(", ")}") + val possibleKeys: List = buildList { + add(esc) + trie.getTrieNode(currentKeys)?.visit { stroke, _ -> add(stroke) } + }.sortedBy { injector.parser.toKeyNotation(it) } + +// println("Keys: ${possibleKeys.joinToString(", ")}") val keyGenerator = Generator.integers(0, possibleKeys.lastIndex) .suchThat { injector.parser.toKeyNotation(possibleKeys[it]) !in stinkyKeysList } .map { possibleKeys[it] } val usedKey = env.generateValue(keyGenerator, null) - val node = currentNode[usedKey] - - env.logMessage("Use command: ${injector.parser.toKeyNotation(usedKey)}. ${if (node is CommandNode) "Action: ${node.actionHolder.actionId}" else ""}") + val node = trie.getTrieNode(currentKeys + usedKey) + env.logMessage("Use command: ${injector.parser.toKeyNotation(currentKeys + usedKey)}. ${if (node?.data != null) "Action: ${node.data!!.actionId}" else ""}") VimNoWriteActionTestCase.typeText(listOf(usedKey), editor, editor.project) IdeEventQueue.getInstance().flushQueue() 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 b1a43aad03..6d058466be 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 @@ -7,14 +7,12 @@ */ package com.maddyhome.idea.vim -import com.maddyhome.idea.vim.action.change.LazyVimCommand import com.maddyhome.idea.vim.api.ExecutionContext import com.maddyhome.idea.vim.api.VimEditor import com.maddyhome.idea.vim.api.globalOptions import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.command.Command import com.maddyhome.idea.vim.command.CommandFlags -import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.command.MappingProcessor import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.diagnostic.VimLogger @@ -23,7 +21,6 @@ import com.maddyhome.idea.vim.diagnostic.vimLogger import com.maddyhome.idea.vim.impl.state.toMappingMode 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 @@ -269,7 +266,7 @@ class KeyHandler { editor.isReplaceCharacter = false editor.resetOpPending() keyHandlerState.partialReset(editor.mode) - keyHandlerState.commandBuilder.resetAll(getKeyRoot(editor.mode.toMappingMode())) + keyHandlerState.commandBuilder.resetAll(injector.keyGroup.getBuiltinCommandsTrie(editor.mode.toMappingMode())) } // TODO we should have a single reset method @@ -277,11 +274,7 @@ class KeyHandler { logger.trace { "Reset is executed" } injector.commandLine.getActiveCommandLine()?.clearCurrentAction() keyHandlerState.partialReset(mode) - keyState.commandBuilder.resetAll(getKeyRoot(mode.toMappingMode())) - } - - private fun getKeyRoot(mappingMode: MappingMode): RootNode { - return injector.keyGroup.getKeyRoot(mappingMode) + keyState.commandBuilder.resetAll(injector.keyGroup.getBuiltinCommandsTrie(mode.toMappingMode())) } fun updateState(keyState: KeyHandlerState) { 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 5733bbc45b..43c3d29fae 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 @@ -12,15 +12,19 @@ import com.maddyhome.idea.vim.command.MappingMode import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.key.KeyMapping import com.maddyhome.idea.vim.key.KeyMappingLayer +import com.maddyhome.idea.vim.key.KeyStrokeTrie 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): RootNode + @Suppress("DEPRECATION") + @Deprecated("Use getBuiltinCommandTrie", ReplaceWith("getBuiltinCommandsTrie(mappingMode)")) + fun getKeyRoot(mappingMode: MappingMode): com.maddyhome.idea.vim.key.CommandPartNode + + fun getBuiltinCommandsTrie(mappingMode: MappingMode): KeyStrokeTrie fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer fun getActions(editor: VimEditor, keyStroke: KeyStroke): List fun getKeymapConflicts(keyStroke: KeyStroke): List 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 5395e9c833..456fffa13e 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 @@ -14,6 +14,7 @@ import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.handler.EditorActionHandlerBase import com.maddyhome.idea.vim.key.KeyMapping import com.maddyhome.idea.vim.key.KeyMappingLayer +import com.maddyhome.idea.vim.key.KeyStrokeTrie import com.maddyhome.idea.vim.key.MappingInfo import com.maddyhome.idea.vim.key.MappingOwner import com.maddyhome.idea.vim.key.RequiredShortcut @@ -29,7 +30,7 @@ abstract class VimKeyGroupBase : VimKeyGroup { @JvmField val myShortcutConflicts: MutableMap = LinkedHashMap() val requiredShortcutKeys: MutableSet = HashSet(300) - val keyRoots: MutableMap> = EnumMap(MappingMode::class.java) + val builtinCommands: MutableMap> = EnumMap(MappingMode::class.java) val keyMappings: MutableMap = EnumMap(MappingMode::class.java) override fun removeKeyMapping(modes: Set, keys: List) { @@ -56,13 +57,19 @@ abstract class VimKeyGroupBase : VimKeyGroup { keyMappings.clear() } + @Suppress("DEPRECATION") + @Deprecated("Use getBuiltinCommandTrie", ReplaceWith("getBuiltinCommandsTrie(mappingMode)")) + override fun getKeyRoot(mappingMode: MappingMode): com.maddyhome.idea.vim.key.CommandPartNode = + RootNode(getBuiltinCommandsTrie(mappingMode)) + /** - * Returns the root of the key mapping for the given mapping mode + * Returns the root node of the builtin command keystroke trie * * @param mappingMode The mapping mode - * @return The key mapping tree root + * @return The root node of the builtin command trie */ - override fun getKeyRoot(mappingMode: MappingMode): RootNode = keyRoots.getOrPut(mappingMode) { RootNode(mappingMode.name.get(0).lowercase()) } + override fun getBuiltinCommandsTrie(mappingMode: MappingMode): KeyStrokeTrie = + builtinCommands.getOrPut(mappingMode) { KeyStrokeTrie(mappingMode.name[0].lowercase()) } override fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer = getKeyMapping(mode) @@ -75,6 +82,7 @@ abstract class VimKeyGroupBase : VimKeyGroup { for (mappingMode in mappingModes) { checkIdentity(mappingMode, action.id, keys) } + @Suppress("DEPRECATION") checkCorrectCombination(action, keys) } @@ -236,6 +244,6 @@ abstract class VimKeyGroupBase : VimKeyGroup { } override fun unregisterCommandActions() { - keyRoots.clear() + builtinCommands.clear() } } 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 1bf45cb41b..1e1dcf0365 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 @@ -20,20 +20,19 @@ import com.maddyhome.idea.vim.handler.MotionActionHandler import com.maddyhome.idea.vim.handler.TextObjectActionHandler import com.maddyhome.idea.vim.helper.StrictMode import com.maddyhome.idea.vim.helper.noneOfEnum -import com.maddyhome.idea.vim.key.CommandNode -import com.maddyhome.idea.vim.key.CommandPartNode -import com.maddyhome.idea.vim.key.RootNode +import com.maddyhome.idea.vim.key.KeyStrokeTrie import org.jetbrains.annotations.TestOnly import javax.swing.KeyStroke class CommandBuilder private constructor( - private var currentCommandPartNode: CommandPartNode, + private var keyStrokeTrie: KeyStrokeTrie, private val counts: MutableList, - private val keyList: MutableList, + private val typedKeyStrokes: MutableList, + private val commandKeyStrokes: MutableList ) : Cloneable { - constructor(rootNode: RootNode, initialUncommittedRawCount: Int = 0) - : this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf()) + constructor(keyStrokeTrie: KeyStrokeTrie, initialUncommittedRawCount: Int = 0) + : this(keyStrokeTrie, mutableListOf(initialUncommittedRawCount), mutableListOf(), mutableListOf()) private var commandState: CurrentCommandState = CurrentCommandState.NEW_COMMAND private var selectedRegister: Char? = null @@ -51,7 +50,7 @@ class CommandBuilder private constructor( } /** Provide the typed keys for `'showcmd'` */ - val keys: Iterable get() = keyList + val keys: Iterable get() = typedKeyStrokes /** Returns true if the command builder is clean and ready to start building */ val isEmpty @@ -167,12 +166,12 @@ class CommandBuilder private constructor( if (currentCount < 0) { currentCount = 999999999 } - addKey(key) + addTypedKeyStroke(key) } fun deleteCountCharacter() { currentCount /= 10 - keyList.removeAt(keyList.size - 1) + typedKeyStrokes.removeLast() } var isRegisterPending: Boolean = false @@ -180,7 +179,7 @@ class CommandBuilder private constructor( fun startWaitingForRegister(key: KeyStroke) { isRegisterPending = true - addKey(key) + addTypedKeyStroke(key) } fun selectRegister(register: Char) { @@ -197,9 +196,9 @@ class CommandBuilder private constructor( * Only public use is when entering a digraph/literal, where each key isn't handled by [CommandBuilder], but should * be added to the `'showcmd'` output. */ - fun addKey(key: KeyStroke) { + fun addTypedKeyStroke(key: KeyStroke) { logger.trace { "added key to command builder: $key" } - keyList.add(key) + typedKeyStrokes.add(key) } /** @@ -268,24 +267,26 @@ class CommandBuilder private constructor( * part node. */ fun processKey(key: KeyStroke, processor: (EditorActionHandlerBase) -> Unit): Boolean { - val node = currentCommandPartNode[key] - when (node) { - is CommandNode -> { - 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 ($key) - ${node.debugString}" } - currentCommandPartNode = node - addKey(key) - return true - } + commandKeyStrokes.add(key) + val node = keyStrokeTrie.getTrieNode(commandKeyStrokes) + if (node == null) { + logger.trace { "No command or part command for key sequence: ${injector.parser.toPrintableString(commandKeyStrokes)}" } + commandKeyStrokes.clear() + return false + } + + addTypedKeyStroke(key) + + val command = node.data + if (command == null) { + logger.trace { "Found unfinished key sequence for ${injector.parser.toPrintableString(commandKeyStrokes)} - ${node.debugString}"} + return true } - logger.trace { "No command/command part node found for key: $key" } - return false + logger.trace { "Found command for ${injector.parser.toPrintableString(commandKeyStrokes)} - ${node.debugString}"} + commandKeyStrokes.clear() + processor(command.instance) + return true } /** @@ -319,8 +320,8 @@ class CommandBuilder private constructor( // Similarly, nmap a s should not try to map the second in // Note that we might still be at RootNode if we're handling a prefix, because we might be buffering keys until we // get a match. This means we'll still process the rest of the keys of the prefix. - val isMultikey = currentCommandPartNode !is RootNode - logger.debug { "Building multikey command: $isMultikey" } + val isMultikey = commandKeyStrokes.isNotEmpty() + logger.debug { "Building multikey command: $commandKeyStrokes" } return isMultikey } @@ -332,21 +333,22 @@ class CommandBuilder private constructor( fun buildCommand(): Command { val rawCount = calculateCount0Snapshot() val command = Command(selectedRegister, rawCount, action!!, argument, action!!.type, action?.flags ?: noneOfEnum()) - resetAll(currentCommandPartNode.root as RootNode) + resetAll(keyStrokeTrie) return command } - fun resetAll(rootNode: RootNode) { + fun resetAll(keyStrokeTrie: KeyStrokeTrie) { logger.trace("resetAll is executed") - currentCommandPartNode = rootNode + this.keyStrokeTrie = keyStrokeTrie commandState = CurrentCommandState.NEW_COMMAND + commandKeyStrokes.clear() counts.clear() counts.add(0) isRegisterPending = false selectedRegister = null action = null argument = null - keyList.clear() + typedKeyStrokes.clear() fallbackArgumentType = null } @@ -357,13 +359,16 @@ class CommandBuilder private constructor( * 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) { + fun resetCommandTrie(keyStrokeTrie: KeyStrokeTrie) { logger.trace("resetCommandTrieRootNode is executed") - currentCommandPartNode = rootNode + this.keyStrokeTrie = keyStrokeTrie } @TestOnly - fun getCurrentTrie(): CommandPartNode = currentCommandPartNode + fun getCurrentTrie(): KeyStrokeTrie = keyStrokeTrie + + @TestOnly + fun getCurrentCommandKeys(): List = commandKeyStrokes override fun equals(other: Any?): Boolean { if (this === other) return true @@ -371,12 +376,12 @@ class CommandBuilder private constructor( other as CommandBuilder - if (currentCommandPartNode != other.currentCommandPartNode) return false + if (keyStrokeTrie != other.keyStrokeTrie) return false if (counts != other.counts) return false if (selectedRegister != other.selectedRegister) return false if (action != other.action) return false if (argument != other.argument) return false - if (keyList != other.keyList) return false + if (typedKeyStrokes != other.typedKeyStrokes) return false if (commandState != other.commandState) return false if (expectedArgumentType != other.expectedArgumentType) return false if (fallbackArgumentType != other.fallbackArgumentType) return false @@ -385,12 +390,12 @@ class CommandBuilder private constructor( } override fun hashCode(): Int { - var result = currentCommandPartNode.hashCode() + var result = keyStrokeTrie.hashCode() result = 31 * result + counts.hashCode() result = 31 * result + selectedRegister.hashCode() result = 31 * result + action.hashCode() result = 31 * result + argument.hashCode() - result = 31 * result + keyList.hashCode() + result = 31 * result + typedKeyStrokes.hashCode() result = 31 * result + commandState.hashCode() result = 31 * result + expectedArgumentType.hashCode() result = 31 * result + fallbackArgumentType.hashCode() @@ -399,9 +404,10 @@ class CommandBuilder private constructor( public override fun clone(): CommandBuilder { val result = CommandBuilder( - currentCommandPartNode, + keyStrokeTrie, counts.toMutableList(), - keyList.toMutableList() + typedKeyStrokes.toMutableList(), + commandKeyStrokes.toMutableList() ) result.selectedRegister = selectedRegister result.action = action @@ -413,12 +419,12 @@ class CommandBuilder private constructor( override fun toString(): String { return "Command state = $commandState, " + - "key list = ${ injector.parser.toKeyNotation(keyList) }, " + + "key list = ${ injector.parser.toKeyNotation(typedKeyStrokes) }, " + "selected register = $selectedRegister, " + "counts = $counts, " + "action = $action, " + "argument = $argument, " + - "command part node - $currentCommandPartNode" + "command part node - $keyStrokeTrie" } companion object { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt new file mode 100644 index 0000000000..ca4d35b5b4 --- /dev/null +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt @@ -0,0 +1,156 @@ +/* + * Copyright 2003-2024 The IdeaVim authors + * + * Use of this source code is governed by an MIT-style + * license that can be found in the LICENSE.txt file or at + * https://opensource.org/licenses/MIT. + */ + +package com.maddyhome.idea.vim.key + +import com.maddyhome.idea.vim.api.injector +import javax.swing.KeyStroke + +/** + * A trie data structure for storing and retrieving values associated with sequences of keystrokes + * + * All leaves will have data, but it is not a requirement for nodes with data to have no children. + * + * @param name The name of this KeyStrokeTrie instance (for debug purposes) + */ +class KeyStrokeTrie(private val name: String) { + interface TrieNode { + val data: T? + + fun visit(visitor: (KeyStroke, TrieNode) -> Unit) + + val debugString: String + } + + private class TrieNodeImpl(val name: String, val depth: Int, override val data: T?) + : TrieNode { + + val children = lazy { mutableMapOf>() } + + override fun visit(visitor: (KeyStroke, TrieNode) -> Unit) { + if (!children.isInitialized()) return + children.value.forEach { visitor(it.key, it.value) } + } + + /** + * Debug helpers to dump this node and its children + */ + override val debugString + get() = buildString { dump(this) } + + private fun dump(builder: StringBuilder) { + builder.run { + append("TrieNode('") + append(name) + append("'") + if (data != null) { + append(", ") + append(data) + } + if (children.isInitialized() && children.value.isNotEmpty()) { + appendLine() + children.value.forEach { + repeat(depth + 1) { append(" ") } + append("'") + append(injector.parser.toKeyNotation(it.key)) + append("' - ") + it.value.dump(this) + if (children.value.size > 1 || depth > 0) appendLine() + } + repeat(depth) { append(" ") } + } + append(")") + } + } + + override fun toString() = "TrieNode('$name', ${children.value.size} children): $data" + } + + private val root = TrieNodeImpl("", 0, null) + + fun visit(visitor: (KeyStroke, TrieNode) -> Unit) { + // Does not visit the (empty) root node + root.visit(visitor) + } + + fun add(keyStrokes: List, data: T) { + var current = root + keyStrokes.forEachIndexed { i, stroke -> + current = current.children.value.getOrPut(stroke) { + val name = current.name + injector.parser.toKeyNotation(stroke) + TrieNodeImpl(name, current.depth + 1, if (i == keyStrokes.lastIndex) data else null) + } + } + } + + /** + * Get the data for the given key sequence if it exists + * + * @return Returns null if the key sequence does not exist, or if the data at the node is empty + */ + fun getData(keyStrokes: List): T? { + var current = root + keyStrokes.forEach { + if (!current.children.isInitialized()) return null + current = current.children.value[it] ?: return null + } + return current.data + } + + /** + * Get the node for the given key sequence if it exists + * + * Like [getData] but will return a node even if that node's data is empty. Will return something useful in the case + * of a matching sequence, or a matching prefix. If it's only a matching prefix, the [TrieNode.data] value will be + * null. + */ + fun getTrieNode(keyStrokes: List): TrieNode? { + var current = root + keyStrokes.forEach { + if (!current.children.isInitialized()) return null + current = current.children.value[it] ?: return null + } + return current + } + + override fun toString(): String { + val children = if (root.children.isInitialized()) { + "${root.children.value.size} children" + } + else { + "0 children (not initialized)" + } + return "KeyStrokeTrie - '$name', $children" + } +} + +fun KeyStrokeTrie.add(keys: String, data: T) { + add(injector.parser.parseKeys(keys), data) +} + +/** + * Returns a map containing all keystroke sequences that start with the given prefix + * + * This only returns keystroke sequences that have associated data. A keystroke sequence without data is considered a + * prefix and not included in the map. + */ +fun KeyStrokeTrie.getPrefixed(prefix: List): Map, T> { + fun visitor(prefix: List, map: MutableMap, T>) { + getTrieNode(prefix)?.let { node -> + node.data?.let { map[prefix] = it } + node.visit { key, value -> visitor(prefix + key, map) } + } + } + + return buildMap { visitor(prefix, this) } +} + +/** + * Returns all keystroke sequences with associated data + */ +fun KeyStrokeTrie.getAll(): Map, T> = getPrefixed(emptyList()) 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 1c01bf2439..99009b5d4a 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 @@ -8,12 +8,15 @@ package com.maddyhome.idea.vim.key +import com.maddyhome.idea.vim.api.VimKeyGroup import com.maddyhome.idea.vim.api.injector import javax.swing.KeyStroke /** * COMPATIBILITY-LAYER: Moved from common package to this one * Please see: https://jb.gg/zo8n0r + * + * Used by idea-which-key (latest is currently 0.10.3) */ /** @@ -39,36 +42,28 @@ import javax.swing.KeyStroke * If the command is complete, it's represented as a [CommandNode]. If this character is a part of command * and the user should complete the sequence, it's [CommandPartNode] */ -@Suppress("GrazieInspection") -interface Node { - val debugString: String - val parent: Node? - - val root: Node - get() = parent?.root ?: this -} +@Deprecated("Use KeyStrokeTrie and VimKeyGroup.getBuiltinCommandsTrie instead") +interface Node /** Represents a complete command */ -data class CommandNode(override val parent: Node, val actionHolder: T, private val name: String) : Node { - override val debugString: String - get() = toString() - - override fun toString() = "COMMAND NODE ($name - ${actionHolder.toString()})" -} - -/** Represents a part of the command */ -open class CommandPartNode( - override val parent: Node?, - internal val name: String, - internal val depth: Int) : Node { - - val children = mutableMapOf>() - - operator fun set(stroke: KeyStroke, node: Node) { - children[stroke] = node +@Suppress("DEPRECATION") +@Deprecated("Use KeyStrokeTrie and VimKeyGroup.getBuiltinCommandsTrie instead") +data class CommandNode(val actionHolder: T) : Node { + override fun toString(): String { + return "COMMAND NODE (${ actionHolder.toString() })" } +} - operator fun get(stroke: KeyStroke): Node? = children[stroke] +/** + * Represents a part of the command + * + * Vim-which-key uses this to get a map of all builtin Vim actions. Sadly, there is on Vim equivalent, so we can't + * provide a Vim script function as an API. After retrieving with [VimKeyGroup.getKeyRoot], the node is iterated + */ +@Suppress("DEPRECATION") +@Deprecated("Use KeyStrokeTrie and VimKeyGroup.getBuiltinCommandsTrie instead") +open class CommandPartNode internal constructor(private val trieNode: KeyStrokeTrie.TrieNode) + : Node, AbstractMap>() { override fun equals(other: Any?): Boolean { if (this === other) return true @@ -79,57 +74,29 @@ open class CommandPartNode( override fun hashCode() = super.hashCode() - 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(name: String) : CommandPartNode(null, name, 0) { - override val debugString: String - get() = "ROOT NODE ($name)\n" + super.debugString - - override fun toString() = "ROOT NODE ($name - ${children.size} children)" -} + override fun toString(): String { + return """ + COMMAND PART NODE( + ${entries.joinToString(separator = "\n") { " " + injector.parser.toKeyNotation(it.key) + " - " + it.value }} + ) + """.trimIndent() + } -fun Node.addLeafs(keyStrokes: List, actionHolder: T) { - var node: Node = this - val len = keyStrokes.size - // Add a child for each keystroke in the shortcut for this action - for (i in 0 until len) { - if (node !is CommandPartNode<*>) { - error("Error in tree constructing") + override val entries: Set>> + get() { + return buildMap { + trieNode.visit { key, value -> + val node: Node = if (value.data == null) { + CommandPartNode(value) + } + else { + CommandNode(value.data!!) + } + put(key, node) + } + }.entries } - node = addNode(node as CommandPartNode, actionHolder, keyStrokes[i], i == len - 1) - } } -private fun addNode(base: CommandPartNode, actionHolder: T, key: KeyStroke, isLastInSequence: Boolean): Node { - val existing = base[key] - if (existing != null) return existing - - val childName = injector.parser.toKeyNotation(key) - val name = when (base) { - is RootNode -> base.name + "_" + childName - else -> base.name + childName - } - val newNode: Node = if (isLastInSequence) { - CommandNode(base, actionHolder, name) - } else { - CommandPartNode(base, name, base.depth + 1) - } - base[key] = newNode - return newNode -} +@Suppress("DEPRECATION") +internal class RootNode(trieNode: KeyStrokeTrie) : CommandPartNode(trieNode.getTrieNode(emptyList())!!) diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DigraphConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DigraphConsumer.kt index b5c5dce8d8..fb1af17417 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DigraphConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/DigraphConsumer.kt @@ -47,12 +47,12 @@ class DigraphConsumer : KeyConsumer { logger.trace("Expected argument is digraph") if (digraphSequence.isDigraphStart(key)) { digraphSequence.startDigraphSequence() - commandBuilder.addKey(key) + commandBuilder.addTypedKeyStroke(key) return true } if (digraphSequence.isLiteralStart(key)) { digraphSequence.startLiteralSequence() - commandBuilder.addKey(key) + commandBuilder.addTypedKeyStroke(key) return true } } @@ -63,7 +63,7 @@ class DigraphConsumer : KeyConsumer { is DigraphResult.Handled -> { keyProcessResultBuilder.addExecutionStep { lambdaKeyState, _, _ -> keyHandler.setPromptCharacterEx(res.promptCharacter) - lambdaKeyState.commandBuilder.addKey(key) + lambdaKeyState.commandBuilder.addTypedKeyStroke(key) } return true } @@ -87,7 +87,7 @@ class DigraphConsumer : KeyConsumer { } val stroke = res.stroke ?: return false keyProcessResultBuilder.addExecutionStep { lambdaKeyState, lambdaEditorState, lambdaContext -> - lambdaKeyState.commandBuilder.addKey(key) + lambdaKeyState.commandBuilder.addTypedKeyStroke(key) keyHandler.handleKey(lambdaEditorState, stroke, lambdaContext, lambdaKeyState) } return true diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt index 34fe4c4f87..21a91264a9 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/consumers/RegisterConsumer.kt @@ -35,7 +35,7 @@ class RegisterConsumer : KeyConsumer { if (!commandBuilder.isRegisterPending) return false logger.trace("Pending mode.") - commandBuilder.addKey(key) + commandBuilder.addTypedKeyStroke(key) val chKey: Char = if (key.keyChar == KeyEvent.CHAR_UNDEFINED) 0.toChar() else key.keyChar handleSelectRegister(chKey, keyProcessResultBuilder) 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 41b2c0a18f..b78b5d637d 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 @@ -23,7 +23,7 @@ data class KeyHandlerState( val editorCommandBuilder: CommandBuilder, var commandLineCommandBuilder: CommandBuilder?, ): Cloneable { - constructor() : this(MappingState(), DigraphSequence(), CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.NORMAL)), null) + constructor() : this(MappingState(), DigraphSequence(), CommandBuilder(injector.keyGroup.getBuiltinCommandsTrie(MappingMode.NORMAL)), null) companion object { private val logger = vimLogger() @@ -57,7 +57,7 @@ data class KeyHandlerState( // argument with the search string. The command has a count of `6`. And a command such as `3:p` becomes an action to // process Ex entry with an argument of `.,.+2p` and a count of 3. The count is ignored by this action. // Note that we use the calculated count. In Vim, `2"a3"b:` transforms to `:.,.+5`, which is the same behaviour - commandLineCommandBuilder = CommandBuilder(injector.keyGroup.getKeyRoot(MappingMode.CMD_LINE), + commandLineCommandBuilder = CommandBuilder(injector.keyGroup.getBuiltinCommandsTrie(MappingMode.CMD_LINE), editorCommandBuilder.calculateCount0Snapshot()) } @@ -68,7 +68,7 @@ data class KeyHandlerState( fun partialReset(mode: Mode) { logger.trace("entered partialReset. mode: $mode") mappingState.resetMappingSequence() - commandBuilder.resetCommandTrieRootNode(injector.keyGroup.getKeyRoot(mode.toMappingMode())) + commandBuilder.resetCommandTrie(injector.keyGroup.getBuiltinCommandsTrie(mode.toMappingMode())) } fun reset(mode: Mode) { @@ -77,7 +77,7 @@ data class KeyHandlerState( mappingState.resetMappingSequence() commandLineCommandBuilder = null - editorCommandBuilder.resetAll(injector.keyGroup.getKeyRoot(mode.toMappingMode())) + editorCommandBuilder.resetAll(injector.keyGroup.getBuiltinCommandsTrie(mode.toMappingMode())) } public override fun clone(): KeyHandlerState { From f646e33bc8379ccb685e612e6965f21594018bfb Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Thu, 31 Oct 2024 00:49:53 +0000 Subject: [PATCH 02/13] Use KeyStrokeTrie for maps --- .../maddyhome/idea/vim/api/VimKeyGroupBase.kt | 2 +- .../idea/vim/command/MappingProcessor.kt | 5 +- .../com/maddyhome/idea/vim/key/KeyMapping.kt | 167 +++++++----------- .../maddyhome/idea/vim/key/KeyMappingLayer.kt | 11 +- .../maddyhome/idea/vim/key/KeyStrokeTrie.kt | 40 ++++- 5 files changed, 111 insertions(+), 114 deletions(-) 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 456fffa13e..08d5ca754c 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 @@ -50,7 +50,7 @@ abstract class VimKeyGroupBase : VimKeyGroup { } override fun getKeyMapping(mode: MappingMode): KeyMapping { - return keyMappings.getOrPut(mode) { KeyMapping() } + return keyMappings.getOrPut(mode) { KeyMapping(mode.name[0].lowercase() + "map") } } override fun resetKeyMappings() { diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt index 0c95470936..e589546113 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/command/MappingProcessor.kt @@ -21,6 +21,7 @@ import com.maddyhome.idea.vim.impl.state.toMappingMode import com.maddyhome.idea.vim.key.KeyConsumer import com.maddyhome.idea.vim.key.KeyMappingLayer import com.maddyhome.idea.vim.key.MappingInfoLayer +import com.maddyhome.idea.vim.key.isPrefix import com.maddyhome.idea.vim.state.KeyHandlerState import javax.swing.KeyStroke @@ -94,7 +95,7 @@ object MappingProcessor: KeyConsumer { // unless a sequence is also a prefix for another mapping. We eagerly evaluate the shortest mapping, so even if a // mapping is a prefix, it will get evaluated when the next character is entered. // Note that currentlyUnhandledKeySequence is the same as the state after commandState.getMappingKeys().add(key). It - // would be nice to tidy ths up + // would be nice to tidy this up if (!mapping.isPrefix(processBuilder.state.mappingState.keys)) { log.debug("There are no mappings that start with the current sequence. Returning false.") return false @@ -161,7 +162,7 @@ object MappingProcessor: KeyConsumer { log.trace("Processing complete mapping sequence...") // The current sequence isn't a prefix, check to see if it's a completed sequence. val mappingState = processBuilder.state.mappingState - val currentMappingInfo = mapping.getLayer(mappingState.keys) + val currentMappingInfo = mapping.getLayer(mappingState.keys.toList()) var mappingInfo = currentMappingInfo if (mappingInfo == null) { log.trace("Haven't found any mapping info for the given sequence. Trying to apply mapping to a subsequence.") diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMapping.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMapping.kt index 44864f3a1b..bde80cad88 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMapping.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMapping.kt @@ -10,8 +10,7 @@ package com.maddyhome.idea.vim.key import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.extension.ExtensionHandler import com.maddyhome.idea.vim.vimscript.model.expressions.Expression -import java.util.function.Consumer -import java.util.stream.Collectors +import org.jetbrains.annotations.TestOnly import javax.swing.KeyStroke /** @@ -20,36 +19,37 @@ import javax.swing.KeyStroke * * @author vlan */ -class KeyMapping : Iterable?>, KeyMappingLayer { - /** - * Contains all key mapping for some mode. - */ - private val myKeys: MutableMap, MappingInfo> = HashMap() - - /** - * Set the contains all possible prefixes for mappings. - * E.g. if there is mapping for "hello", this set will contain "h", "he", "hel", etc. - * Multiset is used to correctly remove the mappings. - */ - private val myPrefixes: MutableMap, Int> = HashMap() - override fun iterator(): MutableIterator> { - return ArrayList(myKeys.keys).iterator() +class KeyMapping(name: String) : Iterable>, KeyMappingLayer { + private val keysTrie = KeyStrokeTrie(name) + + override fun iterator(): Iterator> = ArrayList(keysTrie.getAll().keys).iterator() + + operator fun get(keys: List): MappingInfo? { + keysTrie.getData(keys)?.let { return it } + + getActionNameFromActionMapping(keys)?.let { + return ToActionMappingInfo(it, keys, false, MappingOwner.IdeaVim.System) + } + + return null } + @Deprecated("Use get(List)") operator fun get(keys: Iterable): MappingInfo? { - // Having a parameter of Iterable allows for a nicer API, because we know when a given list is immutable. - // TODO: Should we change this to be a trie? - assert(keys is List<*>) { "keys must be of type List" } - val keyStrokes = keys as List - val mappingInfo = myKeys[keys] - if (mappingInfo != null) return mappingInfo - if (keyStrokes.size > 3) { - if (keyStrokes[0].keyCode == injector.parser.actionKeyStroke.keyCode && keyStrokes[1].keyChar == '(' && keyStrokes[keyStrokes.size - 1].keyChar == ')') { - val builder = StringBuilder() - for (i in 2 until keyStrokes.size - 1) { - builder.append(keyStrokes[i].keyChar) + if (keys is List) { + return get(keys) + } + return get(keys.toList()) + } + + private fun getActionNameFromActionMapping(keys: List): String? { + if (keys.size > 3 + && keys[0].keyCode == injector.parser.actionKeyStroke.keyCode + && keys[1].keyChar == '(' && keys.last().keyChar == ')') { + return buildString { + for (i in 2 until keys.size - 1) { + append(keys[i].keyChar) } - return ToActionMappingInfo(builder.toString(), keyStrokes, false, MappingOwner.IdeaVim.System) } } return null @@ -61,8 +61,7 @@ class KeyMapping : Iterable?>, KeyMappingLayer { extensionHandler: ExtensionHandler, recursive: Boolean, ) { - myKeys[ArrayList(fromKeys)] = ToHandlerMappingInfo(extensionHandler, fromKeys, recursive, owner) - fillPrefixes(fromKeys) + add(fromKeys, ToHandlerMappingInfo(extensionHandler, fromKeys, recursive, owner)) } fun put( @@ -71,8 +70,7 @@ class KeyMapping : Iterable?>, KeyMappingLayer { owner: MappingOwner, recursive: Boolean, ) { - myKeys[ArrayList(fromKeys)] = ToKeysMappingInfo(toKeys, fromKeys, recursive, owner) - fillPrefixes(fromKeys) + add(fromKeys, ToKeysMappingInfo(toKeys, fromKeys, recursive, owner)) } fun put( @@ -82,104 +80,57 @@ class KeyMapping : Iterable?>, KeyMappingLayer { originalString: String, recursive: Boolean, ) { - myKeys[ArrayList(fromKeys)] = - ToExpressionMappingInfo(toExpression, fromKeys, recursive, owner, originalString) - fillPrefixes(fromKeys) + add(fromKeys, ToExpressionMappingInfo(toExpression, fromKeys, recursive, owner, originalString)) } - private fun fillPrefixes(fromKeys: List) { - val prefix: MutableList = ArrayList() - val prefixLength = fromKeys.size - 1 - for (i in 0 until prefixLength) { - prefix.add(fromKeys[i]) - myPrefixes[ArrayList(prefix)] = (myPrefixes[ArrayList(prefix)] ?: 0) + 1 - } + private fun add(keys: List, mappingInfo: MappingInfo) { + keysTrie.add(keys, mappingInfo) } fun delete(owner: MappingOwner) { - val toRemove = myKeys.entries.stream() - .filter { (_, value): Map.Entry, MappingInfo> -> value.owner == owner } - .collect(Collectors.toList()) - toRemove.forEach( - Consumer { (key, value): Map.Entry, MappingInfo> -> - myKeys.remove( - key, - value, - ) - }, - ) - toRemove.map { it.key }.forEach(this::removePrefixes) + getByOwner(owner).forEach { (keys, _) -> + keysTrie.remove(keys) + } } fun delete(keys: List) { - myKeys.remove(keys) ?: return - removePrefixes(keys) + keysTrie.remove(keys) } fun delete() { - myKeys.clear() - myPrefixes.clear() + keysTrie.clear() } - private fun removePrefixes(keys: List) { - val prefix: MutableList = ArrayList() - val prefixLength = keys.size - 1 - for (i in 0 until prefixLength) { - prefix.add(keys[i]) - val existingCount = myPrefixes[prefix] - if (existingCount == 1 || existingCount == null) { - myPrefixes.remove(prefix) - } else { - myPrefixes[prefix] = existingCount - 1 + fun getByOwner(owner: MappingOwner): List, MappingInfo>> = + buildList { + keysTrie.getAll().forEach { (keys, mappingInfo) -> + if (mappingInfo.owner == owner) { + add(Pair(keys, mappingInfo)) + } } } - } - fun getByOwner(owner: MappingOwner): List, MappingInfo>> { - return myKeys.entries.stream() - .filter { (_, value): Map.Entry, MappingInfo> -> value.owner == owner } - .map { (key, value): Map.Entry, MappingInfo> -> - Pair( - key, - value, - ) - }.collect(Collectors.toList()) - } + override fun isPrefix(keys: List): Boolean { + if (keys.isEmpty()) return false - override fun isPrefix(keys: Iterable): Boolean { - // Having a parameter of Iterable allows for a nicer API, because we know when a given list is immutable. - // Perhaps we should look at changing this to a trie or something? - assert(keys is List<*>) { "keys must be of type List" } - val keyList = keys as List - if (keyList.isEmpty()) return false - if (myPrefixes.contains(keys)) return true - val firstChar = keyList[0].keyCode - val lastChar = keyList[keyList.size - 1].keyChar + if (keysTrie.isPrefix(keys)) return true + + val firstChar = keys.first().keyCode + val lastChar = keys.last().keyChar return firstChar == injector.parser.actionKeyStroke.keyCode && lastChar != ')' } - fun hasmapto(toKeys: List): Boolean { - return myKeys.values.stream() - .anyMatch { o: MappingInfo? -> o is ToKeysMappingInfo && o.toKeys == toKeys } + fun hasmapto(toKeys: List) = keysTrie.getAll().any { (_, mappingInfo) -> + mappingInfo is ToKeysMappingInfo && mappingInfo.toKeys == toKeys } - fun hasmapfrom(fromKeys: List): Boolean { - return myKeys.values.stream() - .anyMatch { o: MappingInfo? -> o is ToKeysMappingInfo && o.fromKeys == fromKeys } - } + fun hasmapfrom(fromKeys: List) = keysTrie.getData(fromKeys) != null - fun getMapTo(toKeys: List): List, MappingInfo>> { - return myKeys.entries.stream() - .filter { (_, value): Map.Entry, MappingInfo> -> value is ToKeysMappingInfo && value.toKeys == toKeys } - .map { (key, value): Map.Entry, MappingInfo> -> - Pair( - key, - value, - ) - }.collect(Collectors.toList()) - } + @TestOnly + fun getMapTo(toKeys: List) = + keysTrie.getAll().filter { (_, mappingInfo) -> + mappingInfo is ToKeysMappingInfo && mappingInfo.toKeys == toKeys + }.map { it.toPair() } - override fun getLayer(keys: Iterable): MappingInfoLayer? { - return get(keys) - } + override fun getLayer(keys: List): MappingInfoLayer? = get(keys) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMappingLayer.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMappingLayer.kt index e14e78c89f..7d7c96b8f2 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMappingLayer.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyMappingLayer.kt @@ -11,6 +11,13 @@ package com.maddyhome.idea.vim.key import javax.swing.KeyStroke interface KeyMappingLayer { - fun isPrefix(keys: Iterable): Boolean - fun getLayer(keys: Iterable): MappingInfoLayer? + fun isPrefix(keys: List): Boolean + fun getLayer(keys: List): MappingInfoLayer? +} + +internal fun KeyMappingLayer.isPrefix(keys: Iterable): Boolean { + if (keys is List) { + return isPrefix(keys) + } + return isPrefix(keys.toList()) } diff --git a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt index ca4d35b5b4..a619847612 100644 --- a/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt +++ b/vim-engine/src/main/kotlin/com/maddyhome/idea/vim/key/KeyStrokeTrie.kt @@ -27,7 +27,7 @@ class KeyStrokeTrie(private val name: String) { val debugString: String } - private class TrieNodeImpl(val name: String, val depth: Int, override val data: T?) + private class TrieNodeImpl(val name: String, val depth: Int, override var data: T?) : TrieNode { val children = lazy { mutableMapOf>() } @@ -86,6 +86,9 @@ class KeyStrokeTrie(private val name: String) { TrieNodeImpl(name, current.depth + 1, if (i == keyStrokes.lastIndex) data else null) } } + + // Last write wins (also means we can't cache results) + current.data = data } /** @@ -118,6 +121,41 @@ class KeyStrokeTrie(private val name: String) { return current } + /** + * Returns true if the given keys are a prefix to a longer sequence of keys + * + * Will return true even if the current keys map to a node with data. + */ + fun isPrefix(keyStrokes: List): Boolean { + val node = getTrieNode(keyStrokes) as? TrieNodeImpl ?: return false + return node.children.isInitialized() && node.children.value.isNotEmpty() + } + + fun remove(keys: List) { + val path = buildList { + var current = root + keys.forEach { key -> + if (!current.children.isInitialized()) return + val next = current.children.value[key] ?: return + add(Pair(current, key)) + current = next + } + } + + path.asReversed().forEach { (parent, key) -> + val child = parent.children.value[key] ?: return + if (child.children.isInitialized() && child.children.value.isNotEmpty()) return + parent.children.value.remove(key) + if (parent.children.value.isNotEmpty() || parent.data != null) return + } + } + + fun clear() { + if (root.children.isInitialized()) { + root.children.value.clear() + } + } + override fun toString(): String { val children = if (root.children.isInitialized()) { "${root.children.value.size} children" From e65b55b0e2f1d4cd203425a9b60941d76e850d91 Mon Sep 17 00:00:00 2001 From: Matt Ellis Date: Fri, 1 Nov 2024 17:27:31 +0000 Subject: [PATCH 03/13] Simplify test code --- .../implementation/commands/MapCommandTest.kt | 307 +++++++++--------- 1 file changed, 154 insertions(+), 153 deletions(-) diff --git a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/MapCommandTest.kt b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/MapCommandTest.kt index 159d80c881..30526dd4b6 100644 --- a/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/MapCommandTest.kt +++ b/src/test/java/org/jetbrains/plugins/ideavim/ex/implementation/commands/MapCommandTest.kt @@ -13,7 +13,7 @@ import com.maddyhome.idea.vim.KeyHandler import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.keys import com.maddyhome.idea.vim.command.MappingMode -import com.maddyhome.idea.vim.history.HistoryConstants +import com.maddyhome.idea.vim.history.VimHistory import com.maddyhome.idea.vim.state.mode.Mode import org.jetbrains.plugins.ideavim.ExceptionHandler import org.jetbrains.plugins.ideavim.OnlyThrowLoggedErrorProcessor @@ -32,6 +32,7 @@ import kotlin.test.assertTrue /** * @author vlan */ +@Suppress("SpellCheckingInspection") class MapCommandTest : VimTestCase() { @AfterEach @@ -49,10 +50,10 @@ class MapCommandTest : VimTestCase() { """.trimIndent(), ) - typeText(commandToKeys("nmap k j")) + enterCommand("nmap k j") assertPluginError(false) assertOffset(0) - typeText(injector.parser.parseKeys("k")) + typeText("k") assertOffset(4) } @@ -60,9 +61,9 @@ class MapCommandTest : VimTestCase() { @Test fun testInsertMapJKtoEsc() { configureByText("${c}World!\n") - typeText(commandToKeys("imap jk ")) + enterCommand("imap jk ") assertPluginError(false) - typeText(injector.parser.parseKeys("i" + "Hello, " + "jk")) + typeText("i" + "Hello, " + "jk") assertState("Hello, World!\n") assertMode(Mode.NORMAL()) assertOffset(6) @@ -71,9 +72,9 @@ class MapCommandTest : VimTestCase() { @Test fun testBackslashAtEnd() { configureByText("\n") - typeText(commandToKeys("imap foo\\ bar")) + enterCommand("imap foo\\ bar") assertPluginError(false) - typeText(injector.parser.stringToKeys("ifoo\\")) + typeText("ifoo\\") assertState("bar\n") } @@ -81,8 +82,8 @@ class MapCommandTest : VimTestCase() { @Test fun testUnfinishedSpecialKey() { configureByText("\n") - typeText(commandToKeys("imap bar")) - typeText(injector.parser.stringToKeys("i")) + enterCommand("imap bar") + typeText("i") assertState("bar\n") } @Test fun testMapTable() { configureByText("\n") - typeText(commandToKeys("map gt")) - typeText(commandToKeys("imap foo bar")) - typeText(commandToKeys("imap bar ")) - typeText(commandToKeys("imap gt")) - typeText(commandToKeys("nmap ,f Foo")) - typeText(commandToKeys("nmap Foo iHello")) - typeText(commandToKeys("imap")) + enterCommand("map gt") + enterCommand("imap foo bar") + enterCommand("imap bar ") + enterCommand("imap gt") + enterCommand("nmap ,f Foo") + enterCommand("nmap Foo iHello") + enterCommand("imap") assertExOutput( """ |i gt @@ -112,7 +113,7 @@ class MapCommandTest : VimTestCase() { |i foo bar """.trimMargin(), ) - typeText(commandToKeys("map")) + enterCommand("map") assertExOutput( """ | gt @@ -125,10 +126,10 @@ class MapCommandTest : VimTestCase() { @Test fun testRecursiveMapping() { configureByText("\n") - typeText(commandToKeys("imap foo bar")) - typeText(commandToKeys("imap bar baz")) - typeText(commandToKeys("imap baz quux")) - typeText(injector.parser.parseKeys("i" + "foo")) + enterCommand("imap foo bar") + enterCommand("imap bar baz") + enterCommand("imap baz quux") + typeText("i" + "foo") assertState("quux\n") } @@ -140,8 +141,8 @@ class MapCommandTest : VimTestCase() { Hello 2 """.trimIndent(), ) - typeText(commandToKeys("nmap dc k")) - typeText(injector.parser.parseKeys("dd")) + enterCommand("nmap dc k") + typeText("dd") assertState( """ Hello 2 @@ -152,19 +153,19 @@ class MapCommandTest : VimTestCase() { @Test fun testNonRecursiveMapping() { configureByText("\n") - typeText(commandToKeys("inoremap a b")) + enterCommand("inoremap a b") assertPluginError(false) - typeText(commandToKeys("inoremap b a")) - typeText(injector.parser.parseKeys("i" + "ab")) + enterCommand("inoremap b a") + typeText("i" + "ab") assertState("ba\n") } @Test fun testNonRecursiveMapTable() { configureByText("\n") - typeText(commandToKeys("inoremap jj ")) - typeText(commandToKeys("imap foo bar")) - typeText(commandToKeys("imap")) + enterCommand("inoremap jj ") + enterCommand("imap foo bar") + enterCommand("imap") assertExOutput( """ |i foo bar @@ -183,9 +184,9 @@ class MapCommandTest : VimTestCase() { """.trimIndent(), ) - typeText(commandToKeys("noremap ")) + enterCommand("noremap ") assertPluginError(false) - typeText(injector.parser.parseKeys("l" + "")) + typeText("l" + "") assertPluginError(false) assertState( """ @@ -195,21 +196,21 @@ class MapCommandTest : VimTestCase() { """.trimIndent(), ) assertOffset(1) - typeText(commandToKeys("nmap")) + enterCommand("nmap") assertExOutput("n * ") } @Test fun testIgnoreModifiers() { configureByText("\n") - typeText(commandToKeys("nmap ,a /a")) - typeText(commandToKeys("nmap ,b /b")) - typeText(commandToKeys("nmap ,c /c")) - typeText(commandToKeys("nmap ,d /d")) - typeText(commandToKeys("nmap