diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginDisposableService.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginDisposableService.kt new file mode 100644 index 0000000..d9f1e06 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginDisposableService.kt @@ -0,0 +1,9 @@ +package com.chylex.intellij.keyboardmaster + +import com.intellij.openapi.Disposable +import com.intellij.openapi.components.Service + +@Service +internal class PluginDisposableService : Disposable { + override fun dispose() {} +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt index e678afb..9b1785f 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/PluginStartup.kt @@ -1,6 +1,8 @@ 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.vimNavigation.VimNavigation import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.project.Project import com.intellij.openapi.startup.ProjectActivity @@ -12,13 +14,20 @@ class PluginStartup : ProjectActivity { if (!isInitialized) { isInitialized = true + PluginConfiguration.load() + val application = ApplicationManager.getApplication() if (application.isUnitTestMode) { - CodeCompletionPopupKeyHandler.registerRawHandler() + initialize() } else { - application.invokeLater(CodeCompletionPopupKeyHandler.Companion::registerRawHandler) + application.invokeLater(::initialize) } } } + + private fun initialize() { + CodeCompletionPopupKeyHandler.registerRawHandler() + VimNavigation.register() + } } diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt index 51db572..ca6185e 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfigurable.kt @@ -1,6 +1,8 @@ package com.chylex.intellij.keyboardmaster.configuration 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.dsl.builder.panel import javax.swing.JComponent @@ -12,6 +14,14 @@ class PluginConfigurable : Configurable { private val codeCompletionNextPageShortcut = 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 { return "Keyboard Master" } @@ -23,6 +33,10 @@ class PluginConfigurable : Configurable { row("Next page shortcut:") { cell(codeCompletionNextPageShortcut) } row("Prev page shortcut:") { cell(codeCompletionPrevPageShortcut) } } + + group("Navigation") { + row { cell(enableVimNavigation) } + } } return component @@ -37,6 +51,8 @@ class PluginConfigurable : Configurable { it.codeCompletionItemShortcuts = codeCompletionItemShortcuts.text it.codeCompletionNextPageShortcut = codeCompletionNextPageShortcut.text.firstOrNull()?.code ?: 0 it.codeCompletionPrevPageShortcut = codeCompletionPrevPageShortcut.text.firstOrNull()?.code ?: 0 + + it.enableVimNavigation = enableVimNavigation.isSelected } } @@ -45,6 +61,8 @@ class PluginConfigurable : Configurable { codeCompletionItemShortcuts.text = it.codeCompletionItemShortcuts 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() } + + enableVimNavigation.isSelected = it.enableVimNavigation } } } diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt index 8e08870..1253ba4 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/configuration/PluginConfiguration.kt @@ -1,6 +1,7 @@ package com.chylex.intellij.keyboardmaster.configuration 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.components.PersistentStateComponent import com.intellij.openapi.components.State @@ -16,6 +17,8 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> { var codeCompletionNextPageShortcut: Int = '0'.code var codeCompletionPrevPageShortcut: Int = 0 + var enableVimNavigation = false + companion object { private val instance: PluginConfiguration get() = ApplicationManager.getApplication().getService(PluginConfiguration::class.java) @@ -34,6 +37,7 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> { private fun update(instance: PluginConfiguration) = with(instance) { CodeCompletionPopupConfiguration.updateShortcuts(codeCompletionItemShortcuts, codeCompletionNextPageShortcut, codeCompletionPrevPageShortcut) + VimNavigation.isEnabled = enableVimNavigation } } diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt index 91cc52f..6aa7118 100644 --- a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/codeCompletion/CodeCompletionPopupConfiguration.kt @@ -1,6 +1,5 @@ package com.chylex.intellij.keyboardmaster.feature.codeCompletion -import com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration import com.intellij.util.containers.IntIntHashMap object CodeCompletionPopupConfiguration { @@ -14,10 +13,6 @@ object CodeCompletionPopupConfiguration { val itemShortcutCount get() = hintTexts.size - init { - PluginConfiguration.load() - } - fun updateShortcuts(itemShortcutChars: String, nextPageShortcutCode: Int, previousPageShortcutCode: Int) { charToShortcutMap.clear() diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt new file mode 100644 index 0000000..ae6e084 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/ComponentHolder.kt @@ -0,0 +1,7 @@ +package com.chylex.intellij.keyboardmaster.feature.vimNavigation + +import javax.swing.JComponent + +internal interface ComponentHolder { + val component: JComponent +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt new file mode 100644 index 0000000..779d569 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/KeyStrokeNode.kt @@ -0,0 +1,88 @@ +package com.chylex.intellij.keyboardmaster.feature.vimNavigation + +import com.intellij.openapi.actionSystem.AnActionEvent +import com.intellij.openapi.actionSystem.CustomShortcutSet +import com.intellij.openapi.actionSystem.CustomizedDataContext +import com.intellij.openapi.actionSystem.KeyboardShortcut +import com.intellij.openapi.actionSystem.PlatformDataKeys +import com.intellij.openapi.actionSystem.ex.ActionUtil +import com.intellij.util.containers.map2Array +import java.awt.event.KeyEvent +import javax.swing.KeyStroke + +internal interface KeyStrokeNode<T> { + class Parent<T>(private val keys: Map<KeyStroke, KeyStrokeNode<T>>) : KeyStrokeNode<T> { + val allKeyStrokes: Set<KeyStroke> = mutableSetOf<KeyStroke>().apply { + for ((key, node) in keys) { + add(key) + + if (node is Parent) { + addAll(node.allKeyStrokes) + } + } + } + + fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> { + val keyStroke = when (keyEvent.id) { + KeyEvent.KEY_TYPED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv()) + KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false) + else -> return this + } + + return keys[keyStroke] ?: this + } + + operator fun plus(other: Parent<T>): Parent<T> { + val mergedKeys = HashMap(keys) + + 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 + } + + return Parent(mergedKeys) + } + } + + interface ActionNode<T> : KeyStrokeNode<T> { + fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent) + } + + class IdeaAction<T : ComponentHolder>(private val name: String) : ActionNode<T> { + override fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent) { + val action = actionEvent.actionManager.getAction(name) ?: return + + val dataContext = CustomizedDataContext.create(actionEvent.dataContext) { + when { + PlatformDataKeys.CONTEXT_COMPONENT.`is`(it) -> holder.component + else -> null + } + } + + ActionUtil.invokeAction(action, dataContext, actionEvent.place, null, null) + } + } + + companion object { + fun <T> getAllShortcuts(root: Parent<T>, extra: Set<KeyStroke>? = null): CustomShortcutSet { + val allKeyStrokes = HashSet(root.allKeyStrokes) + + if (extra != null) { + allKeyStrokes.addAll(extra) + } + + for (c in ('a'..'z') + ('A'..'Z')) { + allKeyStrokes.add(KeyStroke.getKeyStroke(c)) + } + + return CustomShortcutSet(*allKeyStrokes.map2Array { KeyboardShortcut(it, null) }) + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt new file mode 100644 index 0000000..0285869 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigation.kt @@ -0,0 +1,32 @@ +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.openapi.application.ApplicationManager +import com.intellij.util.ui.StartupUiUtil +import java.awt.AWTEvent +import java.awt.event.FocusEvent +import javax.swing.JList +import javax.swing.JTable +import javax.swing.JTree + +object VimNavigation { + @Volatile + var isEnabled = false + + fun register() { + 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 && isEnabled) { + when (val source = event.source) { + is JList<*> -> VimListNavigation.install(source) + is JTree -> VimTreeNavigation.install(source) + is JTable -> VimTableNavigation.install(source) + } + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt new file mode 100644 index 0000000..c996695 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/VimNavigationDispatcher.kt @@ -0,0 +1,99 @@ +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.actionSystem.CommonDataKeys +import com.intellij.openapi.actionSystem.DataContext +import com.intellij.openapi.application.ApplicationManager +import com.intellij.openapi.project.DumbAwareAction +import com.intellij.pom.Navigatable +import com.intellij.toolWindow.InternalDecoratorImpl +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.JComponent +import javax.swing.KeyStroke + +internal class VimNavigationDispatcher<T : JComponent>(override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>) : DumbAwareAction(), ComponentHolder { + companion object { + private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java) + private val EXTRA_SHORTCUTS = setOf( + KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), + KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), + ) + + @Suppress("UnstableApiUsage") + fun JComponent.getParentToolWindowId(): String? { + return InternalDecoratorImpl.findNearestDecorator(this)?.toolWindowId + } + } + + private var currentNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>> = rootNode + var isSearching = AtomicBoolean(false) + + init { + registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(rootNode, EXTRA_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, e.dataContext)) { + currentNode = rootNode + 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 handleSpecialKeyPress(keyEvent: KeyEvent, dataContext: DataContext): Boolean { + if (keyEvent.keyCode == KeyEvent.VK_ESCAPE) { + return true + } + + if (keyEvent.keyCode == KeyEvent.VK_ENTER) { + handleEnterKeyPress(dataContext) + return true + } + + return false + } + + private fun handleEnterKeyPress(dataContext: DataContext) { + if (isSearching.compareAndSet(true, false)) { + when (val supply = SpeedSearchSupply.getSupply(component)) { + is SpeedSearchBase<*> -> supply.hidePopup() + is SpeedSearch -> supply.reset() + } + } + else { + val navigatables = dataContext.getData(CommonDataKeys.NAVIGATABLE_ARRAY)?.filter(Navigatable::canNavigate).orEmpty() + for ((index, navigatable) in navigatables.withIndex()) { + navigatable.navigate(index == navigatables.lastIndex) + } + } + } + + 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 + } +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt new file mode 100644 index 0000000..2c25481 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimCommonNavigation.kt @@ -0,0 +1,60 @@ +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 CloseParentToolWindow(), + 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 CloseParentToolWindow<T : JComponent> : ActionNode<VimNavigationDispatcher<T>> { + override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) { + 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) + } + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt new file mode 100644 index 0000000..07e3e96 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimListNavigation.kt @@ -0,0 +1,35 @@ +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.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('j') to IdeaAction("List-selectNextRow"), + KeyStroke.getKeyStroke('k') to IdeaAction("List-selectPreviousRow"), + KeyStroke.getKeyStroke('l') to IdeaAction("List-selectNextColumn"), + ) + ) + + fun install(component: JList<*>) { + if (component.getUserData(KEY) == null) { + component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE)) + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTableNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTableNavigation.kt new file mode 100644 index 0000000..33b48f6 --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTableNavigation.kt @@ -0,0 +1,35 @@ +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('j') to IdeaAction("Table-selectNextRow"), + KeyStroke.getKeyStroke('k') to IdeaAction("Table-selectPreviousRow"), + KeyStroke.getKeyStroke('l') to IdeaAction("Table-selectNextColumn"), + ) + ) + + fun install(component: JTable) { + if (component.getUserData(KEY) == null) { + component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE)) + } + } +} diff --git a/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTreeNavigation.kt b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTreeNavigation.kt new file mode 100644 index 0000000..2ed7c5f --- /dev/null +++ b/src/main/kotlin/com/chylex/intellij/keyboardmaster/feature/vimNavigation/components/VimTreeNavigation.kt @@ -0,0 +1,115 @@ +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 java.awt.event.KeyEvent +import javax.swing.JTree +import javax.swing.KeyStroke + +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('G') to IdeaAction("Tree-selectLast"), + KeyStroke.getKeyStroke('j') to IdeaAction("Tree-selectNext"), + KeyStroke.getKeyStroke('j', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectNextSibling"), + KeyStroke.getKeyStroke('J') to SelectLastSibling, + KeyStroke.getKeyStroke('k') to IdeaAction("Tree-selectPrevious"), + KeyStroke.getKeyStroke('k', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectPreviousSibling"), + KeyStroke.getKeyStroke('K') to SelectFirstSibling, + 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 IdeaAction("CollapseTreeNode"), + ) + ) + + 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 { + tree.expandPath(path) + } + } + } + + 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)) { + tree.collapsePath(path) + } + else { + val parentPath = path.parentPath + if (parentPath.parentPath != null || tree.isRootVisible) { + tree.collapsePath(parentPath) + } + } + } + } + + 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) + + tree.setSelectionRow(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 + } + } + + tree.setSelectionRow(targetRow) + } + } +}