1
0
mirror of https://github.com/chylex/IntelliJ-Keyboard-Master.git synced 2025-09-17 05:24:52 +02:00

Compare commits

..

1 Commits

Author SHA1 Message Date
70aec7b196 Implement vim-style navigation for trees and lists 2024-05-04 03:28:04 +02:00
20 changed files with 213 additions and 838 deletions

View File

@@ -8,7 +8,7 @@ plugins {
} }
group = "com.chylex.intellij.keyboardmaster" group = "com.chylex.intellij.keyboardmaster"
version = "0.6.2" version = "0.4.1"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -16,7 +16,7 @@ repositories {
intellij { intellij {
type.set("IU") type.set("IU")
version.set("2024.2") version.set("2024.1.1")
updateSinceUntilBuild.set(false) updateSinceUntilBuild.set(false)
plugins.add("com.intellij.java") plugins.add("com.intellij.java")
@@ -35,7 +35,7 @@ dependencies {
} }
tasks.patchPluginXml { tasks.patchPluginXml {
sinceBuild.set("242") sinceBuild.set("241")
} }
tasks.test { tasks.test {

View File

@@ -1,8 +1,6 @@
package com.chylex.intellij.keyboardmaster package com.chylex.intellij.keyboardmaster
import com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration
import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupKeyHandler import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupKeyHandler
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigation
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.ProjectActivity import com.intellij.openapi.startup.ProjectActivity
@@ -14,20 +12,13 @@ class PluginStartup : ProjectActivity {
if (!isInitialized) { if (!isInitialized) {
isInitialized = true isInitialized = true
PluginConfiguration.load()
val application = ApplicationManager.getApplication() val application = ApplicationManager.getApplication()
if (application.isUnitTestMode) { if (application.isUnitTestMode) {
initialize() CodeCompletionPopupKeyHandler.registerRawHandler()
} }
else { else {
application.invokeLater(::initialize) application.invokeLater(CodeCompletionPopupKeyHandler.Companion::registerRawHandler)
} }
} }
} }
private fun initialize() {
CodeCompletionPopupKeyHandler.registerRawHandler()
VimNavigation.register()
}
} }

View File

@@ -1,8 +1,6 @@
package com.chylex.intellij.keyboardmaster.configuration package com.chylex.intellij.keyboardmaster.configuration
import com.intellij.openapi.options.Configurable import com.intellij.openapi.options.Configurable
import com.intellij.openapi.ui.Messages
import com.intellij.ui.components.JBCheckBox
import com.intellij.ui.components.JBTextField import com.intellij.ui.components.JBTextField
import com.intellij.ui.dsl.builder.panel import com.intellij.ui.dsl.builder.panel
import javax.swing.JComponent import javax.swing.JComponent
@@ -14,14 +12,6 @@ class PluginConfigurable : Configurable {
private val codeCompletionNextPageShortcut = JBTextField(2) private val codeCompletionNextPageShortcut = JBTextField(2)
private val codeCompletionPrevPageShortcut = JBTextField(2) private val codeCompletionPrevPageShortcut = JBTextField(2)
private val enableVimNavigation = JBCheckBox("Vim-style navigation in lists / trees / tables").also { checkBox ->
checkBox.addActionListener {
if (!checkBox.isSelected) {
Messages.showInfoMessage(checkBox, "Vim-style navigation will be fully disabled after restarting the IDE.", "Keyboard Master")
}
}
}
override fun getDisplayName(): String { override fun getDisplayName(): String {
return "Keyboard Master" return "Keyboard Master"
} }
@@ -33,10 +23,6 @@ class PluginConfigurable : Configurable {
row("Next page shortcut:") { cell(codeCompletionNextPageShortcut) } row("Next page shortcut:") { cell(codeCompletionNextPageShortcut) }
row("Prev page shortcut:") { cell(codeCompletionPrevPageShortcut) } row("Prev page shortcut:") { cell(codeCompletionPrevPageShortcut) }
} }
group("Navigation") {
row { cell(enableVimNavigation) }
}
} }
return component return component
@@ -51,8 +37,6 @@ class PluginConfigurable : Configurable {
it.codeCompletionItemShortcuts = codeCompletionItemShortcuts.text it.codeCompletionItemShortcuts = codeCompletionItemShortcuts.text
it.codeCompletionNextPageShortcut = codeCompletionNextPageShortcut.text.firstOrNull()?.code ?: 0 it.codeCompletionNextPageShortcut = codeCompletionNextPageShortcut.text.firstOrNull()?.code ?: 0
it.codeCompletionPrevPageShortcut = codeCompletionPrevPageShortcut.text.firstOrNull()?.code ?: 0 it.codeCompletionPrevPageShortcut = codeCompletionPrevPageShortcut.text.firstOrNull()?.code ?: 0
it.enableVimNavigation = enableVimNavigation.isSelected
} }
} }
@@ -61,8 +45,6 @@ class PluginConfigurable : Configurable {
codeCompletionItemShortcuts.text = it.codeCompletionItemShortcuts codeCompletionItemShortcuts.text = it.codeCompletionItemShortcuts
codeCompletionNextPageShortcut.text = it.codeCompletionNextPageShortcut.let { code -> if (code == 0) "" else code.toChar().toString() } codeCompletionNextPageShortcut.text = it.codeCompletionNextPageShortcut.let { code -> if (code == 0) "" else code.toChar().toString() }
codeCompletionPrevPageShortcut.text = it.codeCompletionPrevPageShortcut.let { code -> if (code == 0) "" else code.toChar().toString() } codeCompletionPrevPageShortcut.text = it.codeCompletionPrevPageShortcut.let { code -> if (code == 0) "" else code.toChar().toString() }
enableVimNavigation.isSelected = it.enableVimNavigation
} }
} }
} }

View File

@@ -1,7 +1,6 @@
package com.chylex.intellij.keyboardmaster.configuration package com.chylex.intellij.keyboardmaster.configuration
import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupConfiguration import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupConfiguration
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigation
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State import com.intellij.openapi.components.State
@@ -17,8 +16,6 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
var codeCompletionNextPageShortcut: Int = '0'.code var codeCompletionNextPageShortcut: Int = '0'.code
var codeCompletionPrevPageShortcut: Int = 0 var codeCompletionPrevPageShortcut: Int = 0
var enableVimNavigation = false
companion object { companion object {
private val instance: PluginConfiguration private val instance: PluginConfiguration
get() = ApplicationManager.getApplication().getService(PluginConfiguration::class.java) get() = ApplicationManager.getApplication().getService(PluginConfiguration::class.java)
@@ -37,7 +34,6 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
private fun update(instance: PluginConfiguration) = with(instance) { private fun update(instance: PluginConfiguration) = with(instance) {
CodeCompletionPopupConfiguration.updateShortcuts(codeCompletionItemShortcuts, codeCompletionNextPageShortcut, codeCompletionPrevPageShortcut) CodeCompletionPopupConfiguration.updateShortcuts(codeCompletionItemShortcuts, codeCompletionNextPageShortcut, codeCompletionPrevPageShortcut)
VimNavigation.setEnabled(enableVimNavigation)
} }
} }

View File

@@ -2,10 +2,9 @@ package com.chylex.intellij.keyboardmaster.feature.action.gotoError
import com.intellij.codeInsight.CodeInsightActionHandler import com.intellij.codeInsight.CodeInsightActionHandler
import com.intellij.codeInsight.daemon.impl.actions.GotoNextErrorAction import com.intellij.codeInsight.daemon.impl.actions.GotoNextErrorAction
import com.intellij.openapi.actionSystem.DataContext
class GotoNextErrorInOtherModeAction : GotoNextErrorAction() { class GotoNextErrorInOtherModeAction : GotoNextErrorAction() {
override fun getHandler(dataContext: DataContext): CodeInsightActionHandler { override fun getHandler(): CodeInsightActionHandler {
return GotoErrorInOtherModeHandler(forward = true) return GotoErrorInOtherModeHandler(forward = true)
} }
} }

View File

@@ -2,10 +2,9 @@ package com.chylex.intellij.keyboardmaster.feature.action.gotoError
import com.intellij.codeInsight.CodeInsightActionHandler import com.intellij.codeInsight.CodeInsightActionHandler
import com.intellij.codeInsight.daemon.impl.actions.GotoPreviousErrorAction import com.intellij.codeInsight.daemon.impl.actions.GotoPreviousErrorAction
import com.intellij.openapi.actionSystem.DataContext
class GotoPreviousErrorInOtherModeAction : GotoPreviousErrorAction() { class GotoPreviousErrorInOtherModeAction : GotoPreviousErrorAction() {
override fun getHandler(dataContext: DataContext): CodeInsightActionHandler { override fun getHandler(): CodeInsightActionHandler {
return GotoErrorInOtherModeHandler(forward = false) return GotoErrorInOtherModeHandler(forward = false)
} }
} }

View File

@@ -52,7 +52,7 @@ class GotoTypeInFileHandler(private val forward: Boolean) : CodeInsightActionHan
private companion object { private companion object {
fun getNavigationOffsets(file: PsiFile, searchedOffsetRange: IntRange): IntArray { fun getNavigationOffsets(file: PsiFile, searchedOffsetRange: IntRange): IntArray {
val structureViewBuilder = LanguageStructureViewBuilder.getInstance().getStructureViewBuilder(file) val structureViewBuilder = LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(file)
if (structureViewBuilder !is TreeBasedStructureViewBuilder) { if (structureViewBuilder !is TreeBasedStructureViewBuilder) {
return intArrayOf() return intArrayOf()
} }

View File

@@ -1,18 +1,23 @@
package com.chylex.intellij.keyboardmaster.feature.codeCompletion package com.chylex.intellij.keyboardmaster.feature.codeCompletion
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap import com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration
import com.intellij.util.containers.IntIntHashMap
object CodeCompletionPopupConfiguration { object CodeCompletionPopupConfiguration {
const val SHORTCUT_NONE = -1 const val SHORTCUT_NONE = -1
const val SHORTCUT_NEXT_PAGE = -2 const val SHORTCUT_NEXT_PAGE = -2
const val SHORTCUT_PREV_PAGE = -3 const val SHORTCUT_PREV_PAGE = -3
private val charToShortcutMap = Int2IntOpenHashMap(16).also { it.defaultReturnValue(SHORTCUT_NONE) } private val charToShortcutMap = IntIntHashMap(16, SHORTCUT_NONE)
private var hintTexts = mutableListOf<String>() private var hintTexts = mutableListOf<String>()
val itemShortcutCount val itemShortcutCount
get() = hintTexts.size get() = hintTexts.size
init {
PluginConfiguration.load()
}
fun updateShortcuts(itemShortcutChars: String, nextPageShortcutCode: Int, previousPageShortcutCode: Int) { fun updateShortcuts(itemShortcutChars: String, nextPageShortcutCode: Int, previousPageShortcutCode: Int) {
charToShortcutMap.clear() charToShortcutMap.clear()

View File

@@ -1,11 +1,7 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.intellij.ui.popup.WizardPopup
import javax.swing.JComponent import javax.swing.JComponent
internal interface ComponentHolder { internal interface ComponentHolder {
val component: JComponent val component: JComponent
val popup: WizardPopup?
get() = null
} }

View File

@@ -17,37 +17,29 @@ internal interface KeyStrokeNode<T> {
add(key) add(key)
if (node is Parent) { if (node is Parent) {
addAll(node.allKeyStrokes) node.collectKeys(this)
} }
} }
} }
fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> { fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> {
val keyStroke = when { val keyStroke = when (keyEvent.id) {
keyEvent.keyChar != KeyEvent.CHAR_UNDEFINED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv()) KeyEvent.KEY_TYPED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv())
keyEvent.id == KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false) KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false)
else -> return this else -> return this
} }
return keys[keyStroke] ?: this return keys[keyStroke] ?: this
} }
operator fun plus(other: Parent<T>): Parent<T> { private fun collectKeys(target: MutableSet<KeyStroke>) {
val mergedKeys = HashMap(keys) for ((key, node) in keys) {
target.add(key)
for ((otherKey, otherNode) in other.keys) {
if (otherNode is Parent) {
val ourNode = keys[otherKey]
if (ourNode is Parent) {
mergedKeys[otherKey] = ourNode + otherNode
continue
}
}
mergedKeys[otherKey] = otherNode if (node is Parent) {
node.collectKeys(target)
}
} }
return Parent(mergedKeys)
} }
} }
@@ -59,7 +51,7 @@ internal interface KeyStrokeNode<T> {
override fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent) { override fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val action = actionEvent.actionManager.getAction(name) ?: return val action = actionEvent.actionManager.getAction(name) ?: return
val dataContext = CustomizedDataContext.withProvider(actionEvent.dataContext) { val dataContext = CustomizedDataContext.create(actionEvent.dataContext) {
when { when {
PlatformDataKeys.CONTEXT_COMPONENT.`is`(it) -> holder.component PlatformDataKeys.CONTEXT_COMPONENT.`is`(it) -> holder.component
else -> null else -> null
@@ -71,7 +63,7 @@ internal interface KeyStrokeNode<T> {
} }
companion object { companion object {
fun <T> getAllKeyStrokes(root: Parent<T>, extra: Set<KeyStroke>? = null): Set<KeyStroke> { fun <T> getAllShortcuts(root: Parent<T>, extra: Set<KeyStroke>? = null): CustomShortcutSet {
val allKeyStrokes = HashSet(root.allKeyStrokes) val allKeyStrokes = HashSet(root.allKeyStrokes)
if (extra != null) { if (extra != null) {
@@ -82,11 +74,7 @@ internal interface KeyStrokeNode<T> {
allKeyStrokes.add(KeyStroke.getKeyStroke(c)) allKeyStrokes.add(KeyStroke.getKeyStroke(c))
} }
return allKeyStrokes return CustomShortcutSet(*allKeyStrokes.map2Array { KeyboardShortcut(it, null) })
}
fun getAllShortcuts(keyStrokes: Set<KeyStroke>): CustomShortcutSet {
return CustomShortcutSet(*keyStrokes.map2Array { KeyboardShortcut(it, null) })
} }
} }
} }

View File

@@ -1,19 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimListNavigation
import com.intellij.ui.UiInterceptors.PersistentUiInterceptor
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.popup.AbstractPopup
import com.intellij.ui.popup.list.ListPopupImpl
internal object PopupInterceptor : PersistentUiInterceptor<AbstractPopup>(AbstractPopup::class.java) {
override fun shouldIntercept(component: AbstractPopup): Boolean {
if (component is ListPopupImpl && VimNavigation.isEnabled) {
VimListNavigation.install(component.list, component)
}
return false
}
override fun doIntercept(component: AbstractPopup, owner: RelativePoint?) {}
}

View File

@@ -1,75 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.PluginDisposableService
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimListNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTableNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTreeNavigation
import com.intellij.ide.ui.UISettings
import com.intellij.openapi.application.ApplicationManager
import com.intellij.ui.UiInterceptors
import com.intellij.util.ui.StartupUiUtil
import java.awt.AWTEvent
import java.awt.event.FocusEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JList
import javax.swing.JTable
import javax.swing.JTree
import javax.swing.KeyStroke
import javax.swing.UIManager
object VimNavigation {
private val isEnabledFlag = AtomicBoolean(false)
private var originalPopupBindings: Array<*>? = null
val isEnabled
get() = isEnabledFlag.get()
fun register() {
val disposable = ApplicationManager.getApplication().getService(PluginDisposableService::class.java)
StartupUiUtil.addAwtListener(AWTEvent.FOCUS_EVENT_MASK, disposable, ::handleEvent)
UiInterceptors.registerPersistent(disposable, PopupInterceptor)
}
fun setEnabled(enabled: Boolean) {
if (!isEnabledFlag.compareAndSet(!enabled, enabled)) {
return
}
ApplicationManager.getApplication().invokeLater {
if (enabled) {
val originalBindings = (UIManager.get("PopupMenu.selectedWindowInputMapBindings") as Array<*>).also { originalPopupBindings = it }
val updatedBindings = mutableListOf(*originalBindings)
addBinding(updatedBindings, "selectParent", setOf('h', 'p', 'x'))
addBinding(updatedBindings, "selectNext", setOf('j'))
addBinding(updatedBindings, "selectPrevious", setOf('k'))
addBinding(updatedBindings, "selectChild", setOf('l', 'o'))
addBinding(updatedBindings, "cancel", setOf('q'))
UIManager.put("PopupMenu.selectedWindowInputMapBindings", updatedBindings.toTypedArray())
UISettings.getInstance().disableMnemonics = true
}
else {
UIManager.put("PopupMenu.selectedWindowInputMapBindings", originalPopupBindings)
}
}
}
private fun addBinding(bindings: MutableList<Any?>, action: String, chars: Set<Char>) {
for (char in chars) {
bindings.add(KeyStroke.getKeyStroke(char))
bindings.add(action)
}
}
private fun handleEvent(event: AWTEvent) {
if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED && isEnabled) {
when (val source = event.source) {
is JList<*> -> VimListNavigation.install(source)
is JTree -> VimTreeNavigation.install(source)
is JTable -> VimTableNavigation.install(source)
}
}
}
}

View File

@@ -0,0 +1,26 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.PluginDisposableService
import com.intellij.ide.ApplicationInitializedListener
import com.intellij.openapi.application.ApplicationManager
import com.intellij.util.ui.StartupUiUtil
import kotlinx.coroutines.CoroutineScope
import java.awt.AWTEvent
import java.awt.event.FocusEvent
import javax.swing.JTree
@Suppress("UnstableApiUsage")
internal class VimNavigationApplicationListener : ApplicationInitializedListener {
override suspend fun execute(asyncScope: CoroutineScope) {
StartupUiUtil.addAwtListener(::handleEvent, AWTEvent.FOCUS_EVENT_MASK, ApplicationManager.getApplication().getService(PluginDisposableService::class.java))
}
private fun handleEvent(event: AWTEvent) {
if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED) {
val source = event.source
if (source is JTree) {
VimTreeNavigation.install(source)
}
}
}
}

View File

@@ -1,176 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.WrappedAction.ForKeyListener
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.KeyboardShortcut
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.util.SystemInfo
import com.intellij.toolWindow.InternalDecoratorImpl
import com.intellij.ui.SpeedSearchBase
import com.intellij.ui.speedSearch.SpeedSearch
import com.intellij.ui.speedSearch.SpeedSearchSupply
import com.intellij.util.containers.JBIterable
import java.awt.Container
import java.awt.event.ActionEvent
import java.awt.event.ActionListener
import java.awt.event.KeyEvent
import java.beans.PropertyChangeEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.Action
import javax.swing.JComponent
import javax.swing.KeyStroke
internal open class VimNavigationDispatcher<T : JComponent>(final override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>, disposable: Disposable? = null) : DumbAwareAction(), ComponentHolder {
companion object {
@JvmStatic
protected val ENTER_KEY: KeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)
private val CTRL_ENTER_KEY: KeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK)
private val META_ENTER_KEY: KeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.META_DOWN_MASK)
private fun findOriginalEnterAction(component: JComponent): WrappedAction {
var originalEnterAction: WrappedAction? = null
for (container in JBIterable.generate<Container>(component) { it.parent }) {
if (container !is JComponent) {
continue
}
container.getActionForKeyStroke(ENTER_KEY)?.takeUnless(::isIgnoredEnterAction)?.let {
originalEnterAction = WrappedAction.ForActionListener(container, it)
}
for (action in ActionUtil.getActions(container)) {
if (action.shortcutSet.shortcuts.any { it is KeyboardShortcut && it.firstKeyStroke == ENTER_KEY && it.secondKeyStroke == null }) {
originalEnterAction = WrappedAction.ForAnAction(action)
}
}
}
return originalEnterAction ?: ForKeyListener(component)
}
private fun isIgnoredEnterAction(action: ActionListener): Boolean {
return action is Action && action.getValue(Action.NAME) == "toggle"
}
@Suppress("UnstableApiUsage")
fun JComponent.getParentToolWindowId(): String? {
return InternalDecoratorImpl.findNearestDecorator(this)?.toolWindowId
}
}
private val originalEnterAction = findOriginalEnterAction(component)
private var currentNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>> = rootNode
var isSearching = AtomicBoolean(false)
init {
registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(getAllKeyStrokes()), component, disposable)
SpeedSearchSupply.getSupply(component, true)?.addChangeListener(::handleSpeedSearchChange)
}
protected fun getAllKeyStrokes(): Set<KeyStroke> {
return KeyStrokeNode.getAllKeyStrokes(rootNode, setOf(ENTER_KEY, CTRL_ENTER_KEY, META_ENTER_KEY))
}
private fun handleSpeedSearchChange(e: PropertyChangeEvent) {
if (e.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME) {
val speedSearch = e.source as? SpeedSearchSupply ?: return
ApplicationManager.getApplication().invokeLater {
if (!speedSearch.isPopupActive) {
isSearching.set(false)
currentNode = rootNode
}
}
}
}
final override fun actionPerformed(e: AnActionEvent) {
val keyEvent = e.inputEvent as? KeyEvent ?: return
if (keyEvent.id == KeyEvent.KEY_PRESSED && keyEvent.keyCode == KeyEvent.VK_ENTER) {
handleEnterKeyPress(e, keyEvent)
return
}
when (val nextNode = currentNode.getChild(keyEvent)) {
is KeyStrokeNode.Parent<VimNavigationDispatcher<T>> -> currentNode = nextNode
is KeyStrokeNode.ActionNode<VimNavigationDispatcher<T>> -> {
nextNode.performAction(this, e, keyEvent)
currentNode = rootNode
}
}
}
private fun handleEnterKeyPress(actionEvent: AnActionEvent, keyEvent: KeyEvent) {
handleEnterKeyPress(keyEvent) { originalEnterAction.perform(actionEvent, it) }
}
protected inline fun handleEnterKeyPress(keyEvent: KeyEvent, originalAction: (KeyEvent) -> Unit) {
if (isSearching.compareAndSet(true, false) && !runEnterActionImmediately(keyEvent)) {
stopSpeedSearch()
}
else {
currentNode = rootNode
originalAction(keyEvent)
}
}
private fun runEnterActionImmediately(keyEvent: KeyEvent): Boolean {
return if (SystemInfo.isMac) keyEvent.isMetaDown else keyEvent.isControlDown
}
protected open fun stopSpeedSearch() {
when (val supply = SpeedSearchSupply.getSupply(component)) {
is SpeedSearchBase<*> -> supply.hidePopup()
is SpeedSearch -> supply.reset()
}
}
final override fun update(e: AnActionEvent) {
e.presentation.isEnabled = !ignoreEventDueToActiveSearch(e) && !ignoreEventDueToActiveEditing(e)
}
private fun ignoreEventDueToActiveSearch(e: AnActionEvent): Boolean {
return isSearching.get() && !e.inputEvent.let { it is KeyEvent && it.id == KeyEvent.KEY_PRESSED && it.keyCode == KeyEvent.VK_ENTER }
}
private fun ignoreEventDueToActiveEditing(e: AnActionEvent): Boolean {
// Avoid stealing keys from inline text fields.
return e.dataContext.getData(CommonDataKeys.EDITOR) != null
}
final override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
private sealed interface WrappedAction {
fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent)
class ForActionListener(private val component: JComponent, private val listener: ActionListener) : WrappedAction {
override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) {
listener.actionPerformed(ActionEvent(component, ActionEvent.ACTION_PERFORMED, "Enter", keyEvent.`when`, keyEvent.modifiersEx))
}
}
class ForAnAction(val action: AnAction) : WrappedAction {
override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) {
action.actionPerformed(actionEvent)
}
}
class ForKeyListener(private val component: JComponent) : WrappedAction {
override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val unconsumedKeyEvent = KeyEvent(component, keyEvent.id, keyEvent.`when`, keyEvent.modifiersEx, keyEvent.keyCode, keyEvent.keyChar, keyEvent.keyLocation)
component.dispatchEvent(unconsumedKeyEvent)
}
}
}
}

View File

@@ -0,0 +1,157 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.PluginDisposableService
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.ui.getUserData
import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key
import com.intellij.ui.SpeedSearchBase
import com.intellij.ui.speedSearch.SpeedSearch
import com.intellij.ui.speedSearch.SpeedSearchSupply
import java.awt.event.KeyEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JTree
import javax.swing.KeyStroke
internal object VimTreeNavigation {
private val KEY = Key.create<Dispatcher>("KeyboardMaster-VimTreeNavigation")
private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java)
fun install(component: JTree) {
if (component.getUserData(KEY) == null) {
component.putUserData(KEY, Dispatcher(component))
}
}
private val ROOT_NODE = KeyStrokeNode.Parent(
mapOf(
KeyStroke.getKeyStroke('A') to KeyStrokeNode.IdeaAction("MaximizeToolWindow"),
KeyStroke.getKeyStroke('f') to StartSearch,
KeyStroke.getKeyStroke('g') to KeyStrokeNode.Parent(
mapOf(
KeyStroke.getKeyStroke('g') to KeyStrokeNode.IdeaAction("Tree-selectFirst"),
)
),
KeyStroke.getKeyStroke('G') to KeyStrokeNode.IdeaAction("Tree-selectLast"),
KeyStroke.getKeyStroke('j') to KeyStrokeNode.IdeaAction("Tree-selectNext"),
KeyStroke.getKeyStroke('j', KeyEvent.ALT_DOWN_MASK) to KeyStrokeNode.IdeaAction("Tree-selectNextSibling"),
KeyStroke.getKeyStroke('k') to KeyStrokeNode.IdeaAction("Tree-selectPrevious"),
KeyStroke.getKeyStroke('k', KeyEvent.ALT_DOWN_MASK) to KeyStrokeNode.IdeaAction("Tree-selectPreviousSibling"),
KeyStroke.getKeyStroke('m') to KeyStrokeNode.IdeaAction("ShowPopupMenu"),
KeyStroke.getKeyStroke('o') to ExpandOrCollapseTreeNode,
KeyStroke.getKeyStroke('O') to KeyStrokeNode.IdeaAction("FullyExpandTreeNode"),
KeyStroke.getKeyStroke('p') to KeyStrokeNode.IdeaAction("Tree-selectParentNoCollapse"),
KeyStroke.getKeyStroke('q') to KeyStrokeNode.IdeaAction("HideActiveWindow"),
KeyStroke.getKeyStroke('x') to CollapseSelfOrParentNode,
KeyStroke.getKeyStroke('X') to KeyStrokeNode.IdeaAction("CollapseTreeNode"),
KeyStroke.getKeyStroke('/') to StartSearch,
)
)
private val ALL_SHORTCUTS = KeyStrokeNode.getAllShortcuts(
ROOT_NODE,
setOf(
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
)
)
internal class Dispatcher(override val component: JTree) : DumbAwareAction(), ComponentHolder {
private var currentNode: KeyStrokeNode.Parent<Dispatcher> = ROOT_NODE
var isSearching = AtomicBoolean(false)
init {
registerCustomShortcutSet(ALL_SHORTCUTS, component, DISPOSABLE)
SpeedSearchSupply.getSupply(component, true)?.addChangeListener {
if (it.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME && it.oldValue != null && it.newValue == null) {
isSearching.set(false)
}
}
}
override fun actionPerformed(e: AnActionEvent) {
val keyEvent = e.inputEvent as? KeyEvent ?: return
if (keyEvent.id == KeyEvent.KEY_PRESSED && handleSpecialKeyPress(keyEvent)) {
currentNode = ROOT_NODE
return
}
when (val nextNode = currentNode.getChild(keyEvent)) {
is KeyStrokeNode.Parent<Dispatcher> -> currentNode = nextNode
is KeyStrokeNode.ActionNode<Dispatcher> -> {
nextNode.performAction(this, e, keyEvent)
currentNode = ROOT_NODE
}
}
}
private fun handleSpecialKeyPress(keyEvent: KeyEvent): Boolean {
if (keyEvent.keyCode == KeyEvent.VK_ESCAPE) {
return true
}
if (keyEvent.keyCode == KeyEvent.VK_ENTER) {
if (isSearching.compareAndSet(true, false)) {
when (val supply = SpeedSearchSupply.getSupply(component)) {
is SpeedSearchBase<*> -> supply.hidePopup()
is SpeedSearch -> supply.reset()
}
}
return true
}
return false
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = !isSearching.get() || e.inputEvent.let { it is KeyEvent && it.id == KeyEvent.KEY_PRESSED && it.keyCode == KeyEvent.VK_ENTER }
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
}
data object ExpandOrCollapseTreeNode : KeyStrokeNode.ActionNode<Dispatcher> {
override fun performAction(holder: Dispatcher, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
if (tree.isExpanded(path)) {
tree.collapsePath(path)
}
else {
tree.expandPath(path)
}
}
}
data object CollapseSelfOrParentNode : KeyStrokeNode.ActionNode<Dispatcher> {
override fun performAction(holder: Dispatcher, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
if (tree.isExpanded(path)) {
tree.collapsePath(path)
}
else {
val parentPath = path.parentPath
if (parentPath.parentPath != null || tree.isRootVisible) {
tree.collapsePath(parentPath)
}
}
}
}
data object StartSearch : KeyStrokeNode.ActionNode<Dispatcher> {
override fun performAction(holder: Dispatcher, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
holder.isSearching.set(true)
}
}
}

View File

@@ -1,66 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.ActionNode
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.Companion.getParentToolWindowId
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.wm.ToolWindowId
import com.intellij.openapi.wm.ex.ToolWindowManagerEx
import com.intellij.ui.speedSearch.SpeedSearchActivator
import com.intellij.ui.speedSearch.SpeedSearchSupply
import java.awt.event.KeyEvent
import javax.swing.JComponent
import javax.swing.KeyStroke
internal object VimCommonNavigation {
fun <T : JComponent> commonRootNode() = Parent<VimNavigationDispatcher<T>>(
mapOf(
KeyStroke.getKeyStroke('A') to IdeaAction("MaximizeToolWindow"),
KeyStroke.getKeyStroke('f') to StartSearch(),
KeyStroke.getKeyStroke('I') to ToggleExcludedFilesInProjectView(),
KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"),
KeyStroke.getKeyStroke('r') to IdeaAction("SynchronizeCurrentFile"),
KeyStroke.getKeyStroke('R') to IdeaAction("Synchronize"),
KeyStroke.getKeyStroke('q') to CloseParentPopupOrToolWindow(),
KeyStroke.getKeyStroke('/') to StartSearch(),
)
)
private class StartSearch<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> {
@Suppress("UnstableApiUsage")
override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val speedSearch = SpeedSearchSupply.getSupply(holder.component, true) as? SpeedSearchActivator ?: return
if (speedSearch.isAvailable) {
holder.isSearching.set(true)
speedSearch.activate()
}
}
}
private class CloseParentPopupOrToolWindow<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> {
override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val popup = holder.popup
if (popup != null) {
popup.cancel()
return
}
val project = actionEvent.project ?: return
val toolWindowId = holder.component.getParentToolWindowId() ?: return
ToolWindowManagerEx.getInstanceEx(project).hideToolWindow(toolWindowId, true)
}
}
private class ToggleExcludedFilesInProjectView<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> {
private val showExcludedFilesAction = IdeaAction<VimNavigationDispatcher<T>>("ProjectView.ShowExcludedFiles")
override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
if (holder.component.getParentToolWindowId() == ToolWindowId.PROJECT_VIEW) {
showExcludedFilesAction.performAction(holder, actionEvent, keyEvent)
}
}
}
}

View File

@@ -1,118 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.getUserData
import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key
import com.intellij.ui.popup.WizardPopup
import com.intellij.ui.popup.list.ListPopupImpl
import com.intellij.ui.speedSearch.SpeedSearch
import com.intellij.ui.speedSearch.SpeedSearchSupply
import java.awt.event.ActionEvent
import java.awt.event.KeyEvent
import javax.swing.AbstractAction
import javax.swing.JList
import javax.swing.KeyStroke
internal object VimListNavigation {
private val KEY = Key.create<VimNavigationDispatcher<JList<*>>>("KeyboardMaster-VimListNavigation")
private val ROOT_NODE = VimCommonNavigation.commonRootNode<JList<*>>() + Parent(
mapOf(
KeyStroke.getKeyStroke('g') to Parent(
mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("List-selectFirstRow"),
)
),
KeyStroke.getKeyStroke('G') to IdeaAction("List-selectLastRow"),
KeyStroke.getKeyStroke('h') to IdeaAction("List-selectPreviousColumn"),
KeyStroke.getKeyStroke('H') to IdeaAction("List-selectPreviousColumnExtendSelection"),
KeyStroke.getKeyStroke('j') to IdeaAction("List-selectNextRow"),
KeyStroke.getKeyStroke('J') to IdeaAction("List-selectNextRowExtendSelection"),
KeyStroke.getKeyStroke('k') to IdeaAction("List-selectPreviousRow"),
KeyStroke.getKeyStroke('K') to IdeaAction("List-selectPreviousRowExtendSelection"),
KeyStroke.getKeyStroke('l') to IdeaAction("List-selectNextColumn"),
KeyStroke.getKeyStroke('L') to IdeaAction("List-selectNextColumnExtendSelection"),
)
)
private val POPUP_LIST_ROOT_NODE = ROOT_NODE + Parent(
mapOf(
KeyStroke.getKeyStroke('o') to IdeaAction("List-selectNextColumn"),
KeyStroke.getKeyStroke('p') to IdeaAction("List-selectPreviousColumn"),
KeyStroke.getKeyStroke('x') to IdeaAction("List-selectPreviousColumn"),
)
)
fun install(component: JList<*>) {
if (component.getUserData(KEY) == null && component.javaClass.enclosingClass.let { it == null || !WizardPopup::class.java.isAssignableFrom(it) }) {
component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
}
}
fun install(component: JList<*>, popup: WizardPopup) {
if (component.getUserData(KEY) == null) {
component.putUserData(KEY, VimPopupListNavigationDispatcher(component, popup))
}
}
@Suppress("serial")
private class VimPopupListNavigationDispatcher(component: JList<*>, override val popup: WizardPopup) : VimNavigationDispatcher<JList<*>>(component, POPUP_LIST_ROOT_NODE, popup.parent) {
init {
val speedSearch = SpeedSearchSupply.getSupply(component, true) as? SpeedSearch
if (speedSearch != null) {
installSpeedSearch(speedSearch, popup)
}
}
private fun installSpeedSearch(speedSearch: SpeedSearch, popup: WizardPopup) {
val pauseAction = PauseSpeedSearchAction(this, speedSearch)
for (keyStroke in getAllKeyStrokes()) {
if (keyStroke.keyEventType != KeyEvent.KEY_TYPED) {
continue
}
val keyCode = KeyEvent.getExtendedKeyCodeForChar(keyStroke.keyChar.code)
if (keyCode != KeyEvent.VK_UNDEFINED) {
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(keyCode, 0), pauseAction)
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(keyCode, KeyEvent.SHIFT_DOWN_MASK), pauseAction)
}
}
// WizardPopup only checks key codes against its input map, but key codes may be undefined for some characters.
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(KeyEvent.CHAR_UNDEFINED, 0), pauseAction)
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(KeyEvent.CHAR_UNDEFINED, KeyEvent.SHIFT_DOWN_MASK), pauseAction)
if (popup is ListPopupImpl) {
popup.registerAction("KeyboardMaster-VimListNavigation-Enter", ENTER_KEY, object : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
handleEnterKeyPress(createEnterEvent(e)) { popup.handleSelect(true, it) }
}
private fun createEnterEvent(e: ActionEvent): KeyEvent {
return KeyEvent(component, KeyEvent.KEY_PRESSED, e.`when`, e.modifiers, KeyEvent.VK_ENTER, KeyEvent.CHAR_UNDEFINED)
}
})
}
}
override fun stopSpeedSearch() {
val selectedValue = component.selectedValue
super.stopSpeedSearch()
component.setSelectedValue(selectedValue, true)
}
private class PauseSpeedSearchAction(private val dispatcher: VimNavigationDispatcher<JList<*>>, private val speedSearch: SpeedSearch) : AbstractAction() {
override fun actionPerformed(e: ActionEvent) {
if (!dispatcher.isSearching.get()) {
speedSearch.setEnabled(false)
ApplicationManager.getApplication().invokeLater { speedSearch.setEnabled(true) }
}
}
}
}
}

View File

@@ -1,39 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
import com.intellij.openapi.ui.getUserData
import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key
import javax.swing.JTable
import javax.swing.KeyStroke
internal object VimTableNavigation {
private val KEY = Key.create<VimNavigationDispatcher<JTable>>("KeyboardMaster-VimTableNavigation")
private val ROOT_NODE = VimCommonNavigation.commonRootNode<JTable>() + Parent(
mapOf(
KeyStroke.getKeyStroke('g') to Parent(
mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("Table-selectFirstRow"),
)
),
KeyStroke.getKeyStroke('G') to IdeaAction("Table-selectLastRow"),
KeyStroke.getKeyStroke('h') to IdeaAction("Table-selectPreviousColumn"),
KeyStroke.getKeyStroke('H') to IdeaAction("Table-selectPreviousColumnExtendSelection"),
KeyStroke.getKeyStroke('j') to IdeaAction("Table-selectNextRow"),
KeyStroke.getKeyStroke('J') to IdeaAction("Table-selectNextRowExtendSelection"),
KeyStroke.getKeyStroke('k') to IdeaAction("Table-selectPreviousRow"),
KeyStroke.getKeyStroke('K') to IdeaAction("Table-selectPreviousRowExtendSelection"),
KeyStroke.getKeyStroke('l') to IdeaAction("Table-selectNextColumn"),
KeyStroke.getKeyStroke('L') to IdeaAction("Table-selectNextColumnExtendSelection"),
)
)
fun install(component: JTable) {
if (component.getUserData(KEY) == null) {
component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
}
}
}

View File

@@ -1,271 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.ActionNode
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.getUserData
import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key
import com.intellij.ui.ClientProperty
import com.intellij.ui.tree.ui.DefaultTreeUI
import java.awt.event.KeyEvent
import javax.swing.JTree
import javax.swing.KeyStroke
import javax.swing.tree.TreePath
internal object VimTreeNavigation {
private val KEY = Key.create<VimNavigationDispatcher<JTree>>("KeyboardMaster-VimTreeNavigation")
private val ROOT_NODE = VimCommonNavigation.commonRootNode<JTree>() + Parent(
mapOf(
KeyStroke.getKeyStroke('g') to Parent(
mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("Tree-selectFirst"),
KeyStroke.getKeyStroke('j') to SelectLastSibling,
KeyStroke.getKeyStroke('k') to SelectFirstSibling,
KeyStroke.getKeyStroke('o') to ExpandChildrenToNextLevel,
)
),
KeyStroke.getKeyStroke('G') to IdeaAction("Tree-selectLast"),
KeyStroke.getKeyStroke('h') to CollapseSelfOrMoveToParentNode,
KeyStroke.getKeyStroke('H') to CollapseUntilRootNode,
KeyStroke.getKeyStroke('j') to IdeaAction("Tree-selectNext"),
KeyStroke.getKeyStroke('j', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectNextSibling"),
KeyStroke.getKeyStroke('J') to IdeaAction("Tree-selectNextExtendSelection"),
KeyStroke.getKeyStroke('k') to IdeaAction("Tree-selectPrevious"),
KeyStroke.getKeyStroke('k', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectPreviousSibling"),
KeyStroke.getKeyStroke('K') to IdeaAction("Tree-selectPreviousExtendSelection"),
KeyStroke.getKeyStroke('l') to ExpandSelfOrMoveToFirstChildNode,
KeyStroke.getKeyStroke('L') to ExpandUntilFirstLeafNode,
KeyStroke.getKeyStroke('o') to ExpandOrCollapseTreeNode,
KeyStroke.getKeyStroke('O') to IdeaAction("FullyExpandTreeNode"),
KeyStroke.getKeyStroke('p') to IdeaAction("Tree-selectParentNoCollapse"),
KeyStroke.getKeyStroke('P') to IdeaAction("Tree-selectFirst"),
KeyStroke.getKeyStroke('x') to CollapseSelfOrParentNode,
KeyStroke.getKeyStroke('X') to CollapseAll,
)
)
fun install(component: JTree) {
if (component.getUserData(KEY) == null) {
component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
}
}
private data object ExpandOrCollapseTreeNode : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
if (tree.isExpanded(path)) {
tree.collapsePath(path)
}
else {
runWithoutAutoExpand(tree) { tree.expandPath(path) }
}
}
}
private data object ExpandSelfOrMoveToFirstChildNode : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath?.takeUnless { isLeaf(tree, it) } ?: return
if (tree.isExpanded(path)) {
selectRow(tree, getFirstChild(tree, path))
}
else {
runWithoutAutoExpand(tree) { tree.expandPath(path) }
}
}
}
private data object ExpandUntilFirstLeafNode : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
var firstChildPath = path
while (!isLeaf(tree, firstChildPath)) {
tree.expandPath(firstChildPath)
firstChildPath = getFirstChild(tree, firstChildPath)
}
selectRow(tree, firstChildPath)
}
}
private data object CollapseSelfOrMoveToParentNode : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
if (tree.isExpanded(path)) {
collapseAndScroll(tree, path)
}
else {
withParentPath(tree, path) { selectRow(tree, it) }
}
}
}
private data object CollapseSelfOrParentNode : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
if (tree.isExpanded(path)) {
collapseAndScroll(tree, path)
}
else {
withParentPath(tree, path) { collapseAndScroll(tree, it) }
}
}
}
private data object CollapseUntilRootNode : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
var parentPath = path
while (true) {
parentPath = parentPath.parentPath.takeUnless { isInvisibleRoot(tree, it) } ?: break
}
collapseAndScroll(tree, parentPath)
}
}
private data object CollapseAll : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
CollapseUntilRootNode.performAction(holder, actionEvent, keyEvent)
var row = 0
while (row < tree.rowCount) {
tree.collapseRow(row)
row++
}
}
}
private data object ExpandChildrenToNextLevel : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val model = tree.model
val path = tree.selectionPath?.takeUnless { isLeaf(tree, it) } ?: return
var pathsToExpand = mutableListOf(path)
do {
if (pathsToExpand.any(tree::isCollapsed)) {
runWithoutAutoExpand(tree) { pathsToExpand.forEach(tree::expandPath) }
break
}
val nextPathsToExpand = mutableListOf<TreePath>()
for (parentPath in pathsToExpand) {
val lastPathComponent = parentPath.lastPathComponent
for (i in 0 until model.getChildCount(lastPathComponent)) {
val child = model.getChild(lastPathComponent, i)
if (!model.isLeaf(child)) {
nextPathsToExpand.add(parentPath.pathByAddingChild(child))
}
}
}
pathsToExpand = nextPathsToExpand
} while (pathsToExpand.isNotEmpty())
}
}
private data object SelectFirstSibling : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
val parentPath = path.parentPath ?: return
val parentRow = tree.getRowForPath(parentPath)
selectRow(tree, parentRow + 1)
}
}
private data object SelectLastSibling : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
val siblingPathCount = path.pathCount
var testRow = tree.getRowForPath(path)
var targetRow = testRow
while (true) {
testRow++
val testPath = tree.getPathForRow(testRow) ?: break
val testPathCount = testPath.pathCount
if (testPathCount < siblingPathCount) {
break
}
else if (testPathCount == siblingPathCount) {
targetRow = testRow
}
}
selectRow(tree, targetRow)
}
}
private inline fun runWithoutAutoExpand(tree: JTree, action: () -> Unit) {
val previousAutoExpandValue = ClientProperty.get(tree, DefaultTreeUI.AUTO_EXPAND_ALLOWED)
ClientProperty.put(tree, DefaultTreeUI.AUTO_EXPAND_ALLOWED, false)
try {
action()
} finally {
ClientProperty.put(tree, DefaultTreeUI.AUTO_EXPAND_ALLOWED, previousAutoExpandValue)
}
}
private fun selectRow(tree: JTree, row: Int) {
tree.setSelectionRow(row)
tree.scrollRowToVisible(row)
}
private fun selectRow(tree: JTree, path: TreePath) {
selectRow(tree, tree.getRowForPath(path))
}
private fun collapseAndScroll(tree: JTree, path: TreePath) {
tree.collapsePath(path)
tree.scrollRowToVisible(tree.getRowForPath(path))
}
private inline fun withParentPath(tree: JTree, path: TreePath, action: (TreePath) -> Unit) {
val parentPath = path.parentPath
if (!isInvisibleRoot(tree, parentPath)) {
action(parentPath)
}
}
private fun isInvisibleRoot(tree: JTree, parentPath: TreePath): Boolean {
return parentPath.parentPath == null && !tree.isRootVisible
}
private fun getFirstChild(tree: JTree, path: TreePath): TreePath {
return path.pathByAddingChild(tree.model.getChild(path.lastPathComponent, 0))
}
private fun isLeaf(tree: JTree, firstChildPath: TreePath): Boolean {
return tree.model.isLeaf(firstChildPath.lastPathComponent)
}
}

View File

@@ -7,7 +7,6 @@
Collection of keyboard-centric additions. Collection of keyboard-centric additions.
<ul> <ul>
<li>Code completion items can be jumped to using customizable characters (numbers by default).</li> <li>Code completion items can be jumped to using customizable characters (numbers by default).</li>
<li>Vim-style navigation in lists / trees / tables.</li>
<li>Action to go to next/previous highlighted error in other mode.</li> <li>Action to go to next/previous highlighted error in other mode.</li>
<li>Action to go to next/previous type in file.</li> <li>Action to go to next/previous type in file.</li>
</ul> </ul>
@@ -23,6 +22,7 @@
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration" /> <applicationService serviceImplementation="com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration" />
<applicationConfigurable parentId="tools" instance="com.chylex.intellij.keyboardmaster.configuration.PluginConfigurable" id="com.chylex.keyboardmaster" displayName="Keyboard Master" /> <applicationConfigurable parentId="tools" instance="com.chylex.intellij.keyboardmaster.configuration.PluginConfigurable" id="com.chylex.keyboardmaster" displayName="Keyboard Master" />
<applicationInitializedListener implementation="com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationApplicationListener" />
<postStartupActivity implementation="com.chylex.intellij.keyboardmaster.PluginStartup" order="last" /> <postStartupActivity implementation="com.chylex.intellij.keyboardmaster.PluginStartup" order="last" />
</extensions> </extensions>