mirror of
https://github.com/chylex/IntelliJ-IdeaVim.git
synced 2025-06-02 22:34:08 +02:00
Use KeyStrokeTrie for maps
This commit is contained in:
parent
84c7e1159b
commit
2f8fe392af
vim-engine/src/main/kotlin/com/maddyhome/idea/vim
@ -50,7 +50,7 @@ abstract class VimKeyGroupBase : VimKeyGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getKeyMapping(mode: MappingMode): KeyMapping {
|
override fun getKeyMapping(mode: MappingMode): KeyMapping {
|
||||||
return keyMappings.getOrPut(mode) { KeyMapping() }
|
return keyMappings.getOrPut(mode) { KeyMapping(mode.name[0].lowercase() + "map") }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun resetKeyMappings() {
|
override fun resetKeyMappings() {
|
||||||
|
@ -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.KeyConsumer
|
||||||
import com.maddyhome.idea.vim.key.KeyMappingLayer
|
import com.maddyhome.idea.vim.key.KeyMappingLayer
|
||||||
import com.maddyhome.idea.vim.key.MappingInfoLayer
|
import com.maddyhome.idea.vim.key.MappingInfoLayer
|
||||||
|
import com.maddyhome.idea.vim.key.isPrefix
|
||||||
import com.maddyhome.idea.vim.state.KeyHandlerState
|
import com.maddyhome.idea.vim.state.KeyHandlerState
|
||||||
import javax.swing.KeyStroke
|
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
|
// 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.
|
// 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
|
// 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)) {
|
if (!mapping.isPrefix(processBuilder.state.mappingState.keys)) {
|
||||||
log.debug("There are no mappings that start with the current sequence. Returning false.")
|
log.debug("There are no mappings that start with the current sequence. Returning false.")
|
||||||
return false
|
return false
|
||||||
@ -161,7 +162,7 @@ object MappingProcessor: KeyConsumer {
|
|||||||
log.trace("Processing complete mapping sequence...")
|
log.trace("Processing complete mapping sequence...")
|
||||||
// The current sequence isn't a prefix, check to see if it's a completed sequence.
|
// The current sequence isn't a prefix, check to see if it's a completed sequence.
|
||||||
val mappingState = processBuilder.state.mappingState
|
val mappingState = processBuilder.state.mappingState
|
||||||
val currentMappingInfo = mapping.getLayer(mappingState.keys)
|
val currentMappingInfo = mapping.getLayer(mappingState.keys.toList())
|
||||||
var mappingInfo = currentMappingInfo
|
var mappingInfo = currentMappingInfo
|
||||||
if (mappingInfo == null) {
|
if (mappingInfo == null) {
|
||||||
log.trace("Haven't found any mapping info for the given sequence. Trying to apply mapping to a subsequence.")
|
log.trace("Haven't found any mapping info for the given sequence. Trying to apply mapping to a subsequence.")
|
||||||
|
@ -10,8 +10,7 @@ package com.maddyhome.idea.vim.key
|
|||||||
import com.maddyhome.idea.vim.api.injector
|
import com.maddyhome.idea.vim.api.injector
|
||||||
import com.maddyhome.idea.vim.extension.ExtensionHandler
|
import com.maddyhome.idea.vim.extension.ExtensionHandler
|
||||||
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
|
import com.maddyhome.idea.vim.vimscript.model.expressions.Expression
|
||||||
import java.util.function.Consumer
|
import org.jetbrains.annotations.TestOnly
|
||||||
import java.util.stream.Collectors
|
|
||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -20,36 +19,37 @@ import javax.swing.KeyStroke
|
|||||||
*
|
*
|
||||||
* @author vlan
|
* @author vlan
|
||||||
*/
|
*/
|
||||||
class KeyMapping : Iterable<List<KeyStroke?>?>, KeyMappingLayer {
|
class KeyMapping(name: String) : Iterable<List<KeyStroke>>, KeyMappingLayer {
|
||||||
/**
|
private val keysTrie = KeyStrokeTrie<MappingInfo>(name)
|
||||||
* Contains all key mapping for some mode.
|
|
||||||
*/
|
|
||||||
private val myKeys: MutableMap<List<KeyStroke>, MappingInfo> = HashMap()
|
|
||||||
|
|
||||||
/**
|
override fun iterator(): Iterator<List<KeyStroke>> = ArrayList(keysTrie.getAll().keys).iterator()
|
||||||
* Set the contains all possible prefixes for mappings.
|
|
||||||
* E.g. if there is mapping for "hello", this set will contain "h", "he", "hel", etc.
|
operator fun get(keys: List<KeyStroke>): MappingInfo? {
|
||||||
* Multiset is used to correctly remove the mappings.
|
keysTrie.getData(keys)?.let { return it }
|
||||||
*/
|
|
||||||
private val myPrefixes: MutableMap<List<KeyStroke>, Int> = HashMap()
|
getActionNameFromActionMapping(keys)?.let {
|
||||||
override fun iterator(): MutableIterator<List<KeyStroke>> {
|
return ToActionMappingInfo(it, keys, false, MappingOwner.IdeaVim.System)
|
||||||
return ArrayList(myKeys.keys).iterator()
|
}
|
||||||
|
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use get(List<KeyStroke>)")
|
||||||
operator fun get(keys: Iterable<KeyStroke>): MappingInfo? {
|
operator fun get(keys: Iterable<KeyStroke>): MappingInfo? {
|
||||||
// Having a parameter of Iterable allows for a nicer API, because we know when a given list is immutable.
|
if (keys is List<KeyStroke>) {
|
||||||
// TODO: Should we change this to be a trie?
|
return get(keys)
|
||||||
assert(keys is List<*>) { "keys must be of type List<KeyStroke>" }
|
}
|
||||||
val keyStrokes = keys as List<KeyStroke>
|
return get(keys.toList())
|
||||||
val mappingInfo = myKeys[keys]
|
}
|
||||||
if (mappingInfo != null) return mappingInfo
|
|
||||||
if (keyStrokes.size > 3) {
|
private fun getActionNameFromActionMapping(keys: List<KeyStroke>): String? {
|
||||||
if (keyStrokes[0].keyCode == injector.parser.actionKeyStroke.keyCode && keyStrokes[1].keyChar == '(' && keyStrokes[keyStrokes.size - 1].keyChar == ')') {
|
if (keys.size > 3
|
||||||
val builder = StringBuilder()
|
&& keys[0].keyCode == injector.parser.actionKeyStroke.keyCode
|
||||||
for (i in 2 until keyStrokes.size - 1) {
|
&& keys[1].keyChar == '(' && keys.last().keyChar == ')') {
|
||||||
builder.append(keyStrokes[i].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
|
return null
|
||||||
@ -61,8 +61,7 @@ class KeyMapping : Iterable<List<KeyStroke?>?>, KeyMappingLayer {
|
|||||||
extensionHandler: ExtensionHandler,
|
extensionHandler: ExtensionHandler,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
) {
|
) {
|
||||||
myKeys[ArrayList(fromKeys)] = ToHandlerMappingInfo(extensionHandler, fromKeys, recursive, owner)
|
add(fromKeys, ToHandlerMappingInfo(extensionHandler, fromKeys, recursive, owner))
|
||||||
fillPrefixes(fromKeys)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(
|
fun put(
|
||||||
@ -71,8 +70,7 @@ class KeyMapping : Iterable<List<KeyStroke?>?>, KeyMappingLayer {
|
|||||||
owner: MappingOwner,
|
owner: MappingOwner,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
) {
|
) {
|
||||||
myKeys[ArrayList(fromKeys)] = ToKeysMappingInfo(toKeys, fromKeys, recursive, owner)
|
add(fromKeys, ToKeysMappingInfo(toKeys, fromKeys, recursive, owner))
|
||||||
fillPrefixes(fromKeys)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun put(
|
fun put(
|
||||||
@ -82,104 +80,57 @@ class KeyMapping : Iterable<List<KeyStroke?>?>, KeyMappingLayer {
|
|||||||
originalString: String,
|
originalString: String,
|
||||||
recursive: Boolean,
|
recursive: Boolean,
|
||||||
) {
|
) {
|
||||||
myKeys[ArrayList(fromKeys)] =
|
add(fromKeys, ToExpressionMappingInfo(toExpression, fromKeys, recursive, owner, originalString))
|
||||||
ToExpressionMappingInfo(toExpression, fromKeys, recursive, owner, originalString)
|
|
||||||
fillPrefixes(fromKeys)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fillPrefixes(fromKeys: List<KeyStroke>) {
|
private fun add(keys: List<KeyStroke>, mappingInfo: MappingInfo) {
|
||||||
val prefix: MutableList<KeyStroke> = ArrayList()
|
keysTrie.add(keys, mappingInfo)
|
||||||
val prefixLength = fromKeys.size - 1
|
|
||||||
for (i in 0 until prefixLength) {
|
|
||||||
prefix.add(fromKeys[i])
|
|
||||||
myPrefixes[ArrayList(prefix)] = (myPrefixes[ArrayList(prefix)] ?: 0) + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(owner: MappingOwner) {
|
fun delete(owner: MappingOwner) {
|
||||||
val toRemove = myKeys.entries.stream()
|
getByOwner(owner).forEach { (keys, _) ->
|
||||||
.filter { (_, value): Map.Entry<List<KeyStroke>, MappingInfo> -> value.owner == owner }
|
keysTrie.remove(keys)
|
||||||
.collect(Collectors.toList())
|
|
||||||
toRemove.forEach(
|
|
||||||
Consumer { (key, value): Map.Entry<List<KeyStroke>, MappingInfo> ->
|
|
||||||
myKeys.remove(
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
toRemove.map { it.key }.forEach(this::removePrefixes)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete(keys: List<KeyStroke>) {
|
|
||||||
myKeys.remove(keys) ?: return
|
|
||||||
removePrefixes(keys)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun delete() {
|
|
||||||
myKeys.clear()
|
|
||||||
myPrefixes.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun removePrefixes(keys: List<KeyStroke>) {
|
|
||||||
val prefix: MutableList<KeyStroke> = 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<Pair<List<KeyStroke>, MappingInfo>> {
|
fun delete(keys: List<KeyStroke>) {
|
||||||
return myKeys.entries.stream()
|
keysTrie.remove(keys)
|
||||||
.filter { (_, value): Map.Entry<List<KeyStroke>, MappingInfo> -> value.owner == owner }
|
|
||||||
.map { (key, value): Map.Entry<List<KeyStroke>, MappingInfo> ->
|
|
||||||
Pair(
|
|
||||||
key,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}.collect(Collectors.toList())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun isPrefix(keys: Iterable<KeyStroke>): Boolean {
|
fun delete() {
|
||||||
// Having a parameter of Iterable allows for a nicer API, because we know when a given list is immutable.
|
keysTrie.clear()
|
||||||
// Perhaps we should look at changing this to a trie or something?
|
}
|
||||||
assert(keys is List<*>) { "keys must be of type List<KeyStroke>" }
|
|
||||||
val keyList = keys as List<KeyStroke>
|
fun getByOwner(owner: MappingOwner): List<Pair<List<KeyStroke>, MappingInfo>> =
|
||||||
if (keyList.isEmpty()) return false
|
buildList {
|
||||||
if (myPrefixes.contains(keys)) return true
|
keysTrie.getAll().forEach { (keys, mappingInfo) ->
|
||||||
val firstChar = keyList[0].keyCode
|
if (mappingInfo.owner == owner) {
|
||||||
val lastChar = keyList[keyList.size - 1].keyChar
|
add(Pair(keys, mappingInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun isPrefix(keys: List<KeyStroke>): Boolean {
|
||||||
|
if (keys.isEmpty()) return false
|
||||||
|
|
||||||
|
if (keysTrie.isPrefix(keys)) return true
|
||||||
|
|
||||||
|
val firstChar = keys.first().keyCode
|
||||||
|
val lastChar = keys.last().keyChar
|
||||||
return firstChar == injector.parser.actionKeyStroke.keyCode && lastChar != ')'
|
return firstChar == injector.parser.actionKeyStroke.keyCode && lastChar != ')'
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasmapto(toKeys: List<KeyStroke?>): Boolean {
|
fun hasmapto(toKeys: List<KeyStroke>) = keysTrie.getAll().any { (_, mappingInfo) ->
|
||||||
return myKeys.values.stream()
|
mappingInfo is ToKeysMappingInfo && mappingInfo.toKeys == toKeys
|
||||||
.anyMatch { o: MappingInfo? -> o is ToKeysMappingInfo && o.toKeys == toKeys }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasmapfrom(fromKeys: List<KeyStroke?>): Boolean {
|
fun hasmapfrom(fromKeys: List<KeyStroke>) = keysTrie.getData(fromKeys) != null
|
||||||
return myKeys.values.stream()
|
|
||||||
.anyMatch { o: MappingInfo? -> o is ToKeysMappingInfo && o.fromKeys == fromKeys }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getMapTo(toKeys: List<KeyStroke?>): List<Pair<List<KeyStroke>, MappingInfo>> {
|
@TestOnly
|
||||||
return myKeys.entries.stream()
|
fun getMapTo(toKeys: List<KeyStroke?>) =
|
||||||
.filter { (_, value): Map.Entry<List<KeyStroke>, MappingInfo> -> value is ToKeysMappingInfo && value.toKeys == toKeys }
|
keysTrie.getAll().filter { (_, mappingInfo) ->
|
||||||
.map { (key, value): Map.Entry<List<KeyStroke>, MappingInfo> ->
|
mappingInfo is ToKeysMappingInfo && mappingInfo.toKeys == toKeys
|
||||||
Pair(
|
}.map { it.toPair() }
|
||||||
key,
|
|
||||||
value,
|
|
||||||
)
|
|
||||||
}.collect(Collectors.toList())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getLayer(keys: Iterable<KeyStroke>): MappingInfoLayer? {
|
override fun getLayer(keys: List<KeyStroke>): MappingInfoLayer? = get(keys)
|
||||||
return get(keys)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,13 @@ package com.maddyhome.idea.vim.key
|
|||||||
import javax.swing.KeyStroke
|
import javax.swing.KeyStroke
|
||||||
|
|
||||||
interface KeyMappingLayer {
|
interface KeyMappingLayer {
|
||||||
fun isPrefix(keys: Iterable<KeyStroke>): Boolean
|
fun isPrefix(keys: List<KeyStroke>): Boolean
|
||||||
fun getLayer(keys: Iterable<KeyStroke>): MappingInfoLayer?
|
fun getLayer(keys: List<KeyStroke>): MappingInfoLayer?
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun KeyMappingLayer.isPrefix(keys: Iterable<KeyStroke>): Boolean {
|
||||||
|
if (keys is List<KeyStroke>) {
|
||||||
|
return isPrefix(keys)
|
||||||
|
}
|
||||||
|
return isPrefix(keys.toList())
|
||||||
}
|
}
|
||||||
|
@ -27,7 +27,7 @@ class KeyStrokeTrie<T>(private val name: String) {
|
|||||||
val debugString: String
|
val debugString: String
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TrieNodeImpl<T>(val name: String, val depth: Int, override val data: T?)
|
private class TrieNodeImpl<T>(val name: String, val depth: Int, override var data: T?)
|
||||||
: TrieNode<T> {
|
: TrieNode<T> {
|
||||||
|
|
||||||
val children = lazy { mutableMapOf<KeyStroke, TrieNodeImpl<T>>() }
|
val children = lazy { mutableMapOf<KeyStroke, TrieNodeImpl<T>>() }
|
||||||
@ -86,6 +86,9 @@ class KeyStrokeTrie<T>(private val name: String) {
|
|||||||
TrieNodeImpl(name, current.depth + 1, if (i == keyStrokes.lastIndex) data else null)
|
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<T>(private val name: String) {
|
|||||||
return current
|
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<KeyStroke>): Boolean {
|
||||||
|
val node = getTrieNode(keyStrokes) as? TrieNodeImpl<T> ?: return false
|
||||||
|
return node.children.isInitialized() && node.children.value.isNotEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun remove(keys: List<KeyStroke>) {
|
||||||
|
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 {
|
override fun toString(): String {
|
||||||
val children = if (root.children.isInitialized()) {
|
val children = if (root.children.isInitialized()) {
|
||||||
"${root.children.value.size} children"
|
"${root.children.value.size} children"
|
||||||
|
Loading…
Reference in New Issue
Block a user