1
0
mirror of https://github.com/chylex/IntelliJ-IdeaVim.git synced 2025-05-05 00:34:04 +02:00

Introduce KeyStrokeTrie to find commands

Should also restore compatibility with idea-which-key
This commit is contained in:
Matt Ellis 2024-10-30 14:02:34 +00:00 committed by Alex Pláte
parent 18d6f79796
commit 84c7e1159b
13 changed files with 315 additions and 212 deletions
src/main/java/com/maddyhome/idea/vim
extension/nerdtree
group
key
tests/property-tests/src/test/kotlin/org/jetbrains/plugins/ideavim/propertybased
vim-engine/src/main/kotlin/com/maddyhome/idea/vim

View File

@ -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<KeyStroke>()
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<NerdAction> -> {
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<NerdAction> -> 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<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.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<NerdAction> = KeyStrokeTrie<NerdAction>("NERDTree")
private val distinctShortcuts = mutableSetOf<KeyStroke>()
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,

View File

@ -18,7 +18,6 @@ import com.intellij.openapi.components.PersistentStateComponent;
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.VimShortcutKeyAction;
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 class KeyGroup extends VimKeyGroupBase implements PersistentStateComponen
registerRequiredShortcut(keyStrokes, MappingOwner.IdeaVim.System.INSTANCE);
for (MappingMode mappingMode : command.getModes()) {
Node<LazyVimCommand> node = getKeyRoot(mappingMode);
NodesKt.addLeafs(node, keyStrokes, command);
getBuiltinCommandsTrie(mappingMode).add(keyStrokes, command);
}
}
}

View File

@ -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 <T> Node<T>.addLeafs(keys: String, actionHolder: T) {
addLeafs(injector.parser.parseKeys(keys), actionHolder)
}

View File

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

View File

@ -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<LazyVimCommand> {
return injector.keyGroup.getKeyRoot(mappingMode)
keyState.commandBuilder.resetAll(injector.keyGroup.getBuiltinCommandsTrie(mode.toMappingMode()))
}
fun updateState(keyState: KeyHandlerState) {

View File

@ -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<LazyVimCommand>
@Suppress("DEPRECATION")
@Deprecated("Use getBuiltinCommandTrie", ReplaceWith("getBuiltinCommandsTrie(mappingMode)"))
fun getKeyRoot(mappingMode: MappingMode): com.maddyhome.idea.vim.key.CommandPartNode<LazyVimCommand>
fun getBuiltinCommandsTrie(mappingMode: MappingMode): KeyStrokeTrie<LazyVimCommand>
fun getKeyMappingLayer(mode: MappingMode): KeyMappingLayer
fun getActions(editor: VimEditor, keyStroke: KeyStroke): List<NativeAction>
fun getKeymapConflicts(keyStroke: KeyStroke): List<NativeAction>

View File

@ -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<KeyStroke, ShortcutOwnerInfo> = LinkedHashMap()
val requiredShortcutKeys: MutableSet<RequiredShortcut> = HashSet(300)
val keyRoots: MutableMap<MappingMode, RootNode<LazyVimCommand>> = EnumMap(MappingMode::class.java)
val builtinCommands: MutableMap<MappingMode, KeyStrokeTrie<LazyVimCommand>> = EnumMap(MappingMode::class.java)
val keyMappings: MutableMap<MappingMode, KeyMapping> = EnumMap(MappingMode::class.java)
override fun removeKeyMapping(modes: Set<MappingMode>, keys: List<KeyStroke>) {
@ -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<LazyVimCommand> =
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<LazyVimCommand> = keyRoots.getOrPut(mappingMode) { RootNode(mappingMode.name.get(0).lowercase()) }
override fun getBuiltinCommandsTrie(mappingMode: MappingMode): KeyStrokeTrie<LazyVimCommand> =
builtinCommands.getOrPut(mappingMode) { KeyStrokeTrie<LazyVimCommand>(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()
}
}

View File

@ -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<LazyVimCommand>,
private var keyStrokeTrie: KeyStrokeTrie<LazyVimCommand>,
private val counts: MutableList<Int>,
private val keyList: MutableList<KeyStroke>,
private val typedKeyStrokes: MutableList<KeyStroke>,
private val commandKeyStrokes: MutableList<KeyStroke>
) : Cloneable {
constructor(rootNode: RootNode<LazyVimCommand>, initialUncommittedRawCount: Int = 0)
: this(rootNode, mutableListOf(initialUncommittedRawCount), mutableListOf())
constructor(keyStrokeTrie: KeyStrokeTrie<LazyVimCommand>, 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<KeyStroke> get() = keyList
val keys: Iterable<KeyStroke> 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
}
logger.trace { "No command/command part node found for key: $key" }
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 { "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 <C-W>a <C-W>s should not try to map the second <C-W> in <C-W><C-W>
// 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<LazyVimCommand>)
resetAll(keyStrokeTrie)
return command
}
fun resetAll(rootNode: RootNode<LazyVimCommand>) {
fun resetAll(keyStrokeTrie: KeyStrokeTrie<LazyVimCommand>) {
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<LazyVimCommand>) {
fun resetCommandTrie(keyStrokeTrie: KeyStrokeTrie<LazyVimCommand>) {
logger.trace("resetCommandTrieRootNode is executed")
currentCommandPartNode = rootNode
this.keyStrokeTrie = keyStrokeTrie
}
@TestOnly
fun getCurrentTrie(): CommandPartNode<LazyVimCommand> = currentCommandPartNode
fun getCurrentTrie(): KeyStrokeTrie<LazyVimCommand> = keyStrokeTrie
@TestOnly
fun getCurrentCommandKeys(): List<KeyStroke> = 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 {

View File

@ -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<T>(private val name: String) {
interface TrieNode<T> {
val data: T?
fun visit(visitor: (KeyStroke, TrieNode<T>) -> Unit)
val debugString: String
}
private class TrieNodeImpl<T>(val name: String, val depth: Int, override val data: T?)
: TrieNode<T> {
val children = lazy { mutableMapOf<KeyStroke, TrieNodeImpl<T>>() }
override fun visit(visitor: (KeyStroke, TrieNode<T>) -> 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<T>("", 0, null)
fun visit(visitor: (KeyStroke, TrieNode<T>) -> Unit) {
// Does not visit the (empty) root node
root.visit(visitor)
}
fun add(keyStrokes: List<KeyStroke>, 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<KeyStroke>): 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<KeyStroke>): TrieNode<T>? {
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 <T> KeyStrokeTrie<T>.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 <T> KeyStrokeTrie<T>.getPrefixed(prefix: List<KeyStroke>): Map<List<KeyStroke>, T> {
fun visitor(prefix: List<KeyStroke>, map: MutableMap<List<KeyStroke>, 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 <T> KeyStrokeTrie<T>.getAll(): Map<List<KeyStroke>, T> = getPrefixed(emptyList())

View File

@ -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<T> {
val debugString: String
val parent: Node<T>?
val root: Node<T>
get() = parent?.root ?: this
}
@Deprecated("Use KeyStrokeTrie and VimKeyGroup.getBuiltinCommandsTrie instead")
interface Node<T>
/** Represents a complete command */
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()})"
@Suppress("DEPRECATION")
@Deprecated("Use KeyStrokeTrie and VimKeyGroup.getBuiltinCommandsTrie instead")
data class CommandNode<T>(val actionHolder: T) : Node<T> {
override fun toString(): String {
return "COMMAND NODE (${ actionHolder.toString() })"
}
}
/** Represents a part of the command */
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]
/**
* 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<T> internal constructor(private val trieNode: KeyStrokeTrie.TrieNode<T>)
: Node<T>, AbstractMap<KeyStroke, Node<T>>() {
override fun equals(other: Any?): Boolean {
if (this === other) return true
@ -79,57 +74,29 @@ open class CommandPartNode<T>(
override fun hashCode() = super.hashCode()
override fun toString() = "COMMAND PART 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()
}
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(")")
override val entries: Set<Map.Entry<KeyStroke, Node<T>>>
get() {
return buildMap {
trieNode.visit { key, value ->
val node: Node<T> = if (value.data == null) {
CommandPartNode<T>(value)
}
else {
CommandNode(value.data!!)
}
put(key, node)
}
}.entries
}
}
/** Represents a root node for the mode */
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
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")
}
node = addNode(node as CommandPartNode<T>, actionHolder, keyStrokes[i], i == len - 1)
}
}
private fun <T> addNode(base: CommandPartNode<T>, actionHolder: T, key: KeyStroke, isLastInSequence: Boolean): Node<T> {
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<T> = if (isLastInSequence) {
CommandNode(base, actionHolder, name)
} else {
CommandPartNode(base, name, base.depth + 1)
}
base[key] = newNode
return newNode
}
@Suppress("DEPRECATION")
internal class RootNode<T>(trieNode: KeyStrokeTrie<T>) : CommandPartNode<T>(trieNode.getTrieNode(emptyList())!!)

View File

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

View File

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

View File

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