1
0
mirror of https://github.com/chylex/IntelliJ-Keyboard-Master.git synced 2025-09-15 17:32:10 +02:00

Compare commits

..

18 Commits

Author SHA1 Message Date
7596b8727a Release version 0.6.2 2024-09-19 05:36:42 +02:00
38c80a7b27 Change vim-style navigation binding for 'X' to close all tree nodes 2024-09-18 20:00:34 +02:00
efe13712ad Add new vim-style bindings for navigating tree hierarchy 2024-09-18 20:00:29 +02:00
5aea7947ec Disable automatic tree auto-expansion in vim-style navigation 2024-09-18 08:52:51 +02:00
f814ec04bd Release version 0.6.1 2024-08-11 11:22:06 +02:00
c7229a6d8e Fix leaking editor instances 2024-08-11 11:21:49 +02:00
d711e60da8 Release version 0.6.0 2024-08-10 15:35:08 +02:00
356826f6ae Update uses of obsolete APIs for IDEA 2024.2 2024-08-10 15:35:08 +02:00
f3a3685084 Release version 0.5.8 2024-08-05 03:55:32 +02:00
5e654762fc Fix vim-style navigation not scrolling after collapsing a tree node 2024-08-05 03:55:14 +02:00
723b8af939 Release version 0.5.7 2024-07-15 13:08:10 +02:00
72158689ff Add vim-style binding to expand tree to the next level 2024-07-15 13:06:44 +02:00
d76e259c25 Release version 0.5.6 2024-06-18 12:40:58 +02:00
6ba2a0863e Add vim-style bindings for Ctrl/Cmd+Enter to immediately execute Enter action during search 2024-06-18 12:17:12 +02:00
48a9159c87 Release version 0.5.5 2024-05-19 15:58:49 +02:00
844738794b Make vim-style navigation Enter handling during search in popups consistent with other components 2024-05-19 15:58:25 +02:00
0e2928a737 Fix vim-style navigation preventing default Enter action in components with key listeners 2024-05-19 15:54:30 +02:00
b131413c8d Fix vim-style navigation being active in popups even when disabled 2024-05-19 15:13:59 +02:00
11 changed files with 240 additions and 38 deletions

View File

@@ -8,7 +8,7 @@ plugins {
} }
group = "com.chylex.intellij.keyboardmaster" group = "com.chylex.intellij.keyboardmaster"
version = "0.5.4" version = "0.6.2"
repositories { repositories {
mavenCentral() mavenCentral()
@@ -16,7 +16,7 @@ repositories {
intellij { intellij {
type.set("IU") type.set("IU")
version.set("2024.1.1") version.set("2024.2")
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("241") sinceBuild.set("242")
} }
tasks.test { tasks.test {

View File

@@ -2,9 +2,10 @@ 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(): CodeInsightActionHandler { override fun getHandler(dataContext: DataContext): CodeInsightActionHandler {
return GotoErrorInOtherModeHandler(forward = true) return GotoErrorInOtherModeHandler(forward = true)
} }
} }

View File

@@ -2,9 +2,10 @@ 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(): CodeInsightActionHandler { override fun getHandler(dataContext: DataContext): 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.INSTANCE.getStructureViewBuilder(file) val structureViewBuilder = LanguageStructureViewBuilder.getInstance().getStructureViewBuilder(file)
if (structureViewBuilder !is TreeBasedStructureViewBuilder) { if (structureViewBuilder !is TreeBasedStructureViewBuilder) {
return intArrayOf() return intArrayOf()
} }

View File

@@ -1,13 +1,13 @@
package com.chylex.intellij.keyboardmaster.feature.codeCompletion package com.chylex.intellij.keyboardmaster.feature.codeCompletion
import com.intellij.util.containers.IntIntHashMap import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap
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 = IntIntHashMap(16, SHORTCUT_NONE) private val charToShortcutMap = Int2IntOpenHashMap(16).also { it.defaultReturnValue(SHORTCUT_NONE) }
private var hintTexts = mutableListOf<String>() private var hintTexts = mutableListOf<String>()
val itemShortcutCount val itemShortcutCount

View File

@@ -59,7 +59,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.create(actionEvent.dataContext) { val dataContext = CustomizedDataContext.withProvider(actionEvent.dataContext) {
when { when {
PlatformDataKeys.CONTEXT_COMPONENT.`is`(it) -> holder.component PlatformDataKeys.CONTEXT_COMPONENT.`is`(it) -> holder.component
else -> null else -> null

View File

@@ -8,7 +8,7 @@ import com.intellij.ui.popup.list.ListPopupImpl
internal object PopupInterceptor : PersistentUiInterceptor<AbstractPopup>(AbstractPopup::class.java) { internal object PopupInterceptor : PersistentUiInterceptor<AbstractPopup>(AbstractPopup::class.java) {
override fun shouldIntercept(component: AbstractPopup): Boolean { override fun shouldIntercept(component: AbstractPopup): Boolean {
if (component is ListPopupImpl) { if (component is ListPopupImpl && VimNavigation.isEnabled) {
VimListNavigation.install(component.list, component) VimListNavigation.install(component.list, component)
} }

View File

@@ -18,18 +18,21 @@ import javax.swing.KeyStroke
import javax.swing.UIManager import javax.swing.UIManager
object VimNavigation { object VimNavigation {
private val isEnabled = AtomicBoolean(false) private val isEnabledFlag = AtomicBoolean(false)
private var originalPopupBindings: Array<*>? = null private var originalPopupBindings: Array<*>? = null
val isEnabled
get() = isEnabledFlag.get()
fun register() { fun register() {
val disposable = ApplicationManager.getApplication().getService(PluginDisposableService::class.java) val disposable = ApplicationManager.getApplication().getService(PluginDisposableService::class.java)
StartupUiUtil.addAwtListener(::handleEvent, AWTEvent.FOCUS_EVENT_MASK, disposable) StartupUiUtil.addAwtListener(AWTEvent.FOCUS_EVENT_MASK, disposable, ::handleEvent)
UiInterceptors.registerPersistent(disposable, PopupInterceptor) UiInterceptors.registerPersistent(disposable, PopupInterceptor)
} }
fun setEnabled(enabled: Boolean) { fun setEnabled(enabled: Boolean) {
if (!isEnabled.compareAndSet(!enabled, enabled)) { if (!isEnabledFlag.compareAndSet(!enabled, enabled)) {
return return
} }
@@ -61,7 +64,7 @@ object VimNavigation {
} }
private fun handleEvent(event: AWTEvent) { private fun handleEvent(event: AWTEvent) {
if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED && isEnabled.get()) { if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED && isEnabled) {
when (val source = event.source) { when (val source = event.source) {
is JList<*> -> VimListNavigation.install(source) is JList<*> -> VimListNavigation.install(source)
is JTree -> VimTreeNavigation.install(source) is JTree -> VimTreeNavigation.install(source)

View File

@@ -1,6 +1,7 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.PluginDisposableService 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.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.actionSystem.AnAction
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
@@ -9,6 +10,7 @@ import com.intellij.openapi.actionSystem.KeyboardShortcut
import com.intellij.openapi.actionSystem.ex.ActionUtil import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.DumbAwareAction import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.util.SystemInfo
import com.intellij.toolWindow.InternalDecoratorImpl import com.intellij.toolWindow.InternalDecoratorImpl
import com.intellij.ui.SpeedSearchBase import com.intellij.ui.SpeedSearchBase
import com.intellij.ui.speedSearch.SpeedSearch import com.intellij.ui.speedSearch.SpeedSearch
@@ -20,15 +22,19 @@ import java.awt.event.ActionListener
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import java.beans.PropertyChangeEvent import java.beans.PropertyChangeEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.Action
import javax.swing.JComponent import javax.swing.JComponent
import javax.swing.KeyStroke import javax.swing.KeyStroke
internal open class VimNavigationDispatcher<T : JComponent>(final override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>) : DumbAwareAction(), ComponentHolder { 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 { companion object {
private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java) @JvmStatic
private val ENTER_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0) protected val ENTER_KEY: KeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0)
private fun findOriginalEnterAction(component: JComponent): WrappedAction? { 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 var originalEnterAction: WrappedAction? = null
for (container in JBIterable.generate<Container>(component) { it.parent }) { for (container in JBIterable.generate<Container>(component) { it.parent }) {
@@ -36,7 +42,7 @@ internal open class VimNavigationDispatcher<T : JComponent>(final override val c
continue continue
} }
container.getActionForKeyStroke(ENTER_KEY)?.let { container.getActionForKeyStroke(ENTER_KEY)?.takeUnless(::isIgnoredEnterAction)?.let {
originalEnterAction = WrappedAction.ForActionListener(container, it) originalEnterAction = WrappedAction.ForActionListener(container, it)
} }
@@ -47,7 +53,11 @@ internal open class VimNavigationDispatcher<T : JComponent>(final override val c
} }
} }
return originalEnterAction return originalEnterAction ?: ForKeyListener(component)
}
private fun isIgnoredEnterAction(action: ActionListener): Boolean {
return action is Action && action.getValue(Action.NAME) == "toggle"
} }
@Suppress("UnstableApiUsage") @Suppress("UnstableApiUsage")
@@ -61,12 +71,12 @@ internal open class VimNavigationDispatcher<T : JComponent>(final override val c
var isSearching = AtomicBoolean(false) var isSearching = AtomicBoolean(false)
init { init {
registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(getAllKeyStrokes()), component, DISPOSABLE) registerCustomShortcutSet(KeyStrokeNode.getAllShortcuts(getAllKeyStrokes()), component, disposable)
SpeedSearchSupply.getSupply(component, true)?.addChangeListener(::handleSpeedSearchChange) SpeedSearchSupply.getSupply(component, true)?.addChangeListener(::handleSpeedSearchChange)
} }
protected fun getAllKeyStrokes(): Set<KeyStroke> { protected fun getAllKeyStrokes(): Set<KeyStroke> {
return KeyStrokeNode.getAllKeyStrokes(rootNode, setOf(ENTER_KEY)) return KeyStrokeNode.getAllKeyStrokes(rootNode, setOf(ENTER_KEY, CTRL_ENTER_KEY, META_ENTER_KEY))
} }
private fun handleSpeedSearchChange(e: PropertyChangeEvent) { private fun handleSpeedSearchChange(e: PropertyChangeEvent) {
@@ -100,15 +110,27 @@ internal open class VimNavigationDispatcher<T : JComponent>(final override val c
} }
private fun handleEnterKeyPress(actionEvent: AnActionEvent, keyEvent: KeyEvent) { private fun handleEnterKeyPress(actionEvent: AnActionEvent, keyEvent: KeyEvent) {
if (isSearching.compareAndSet(true, false)) { handleEnterKeyPress(keyEvent) { originalEnterAction.perform(actionEvent, it) }
when (val supply = SpeedSearchSupply.getSupply(component)) { }
is SpeedSearchBase<*> -> supply.hidePopup()
is SpeedSearch -> supply.reset() protected inline fun handleEnterKeyPress(keyEvent: KeyEvent, originalAction: (KeyEvent) -> Unit) {
} if (isSearching.compareAndSet(true, false) && !runEnterActionImmediately(keyEvent)) {
stopSpeedSearch()
} }
else { else {
currentNode = rootNode currentNode = rootNode
originalEnterAction?.perform(actionEvent, keyEvent) 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()
} }
} }
@@ -132,7 +154,7 @@ internal open class VimNavigationDispatcher<T : JComponent>(final override val c
private sealed interface WrappedAction { private sealed interface WrappedAction {
fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent)
class ForActionListener(val component: JComponent, val listener: ActionListener) : WrappedAction { class ForActionListener(private val component: JComponent, private val listener: ActionListener) : WrappedAction {
override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) { override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) {
listener.actionPerformed(ActionEvent(component, ActionEvent.ACTION_PERFORMED, "Enter", keyEvent.`when`, keyEvent.modifiersEx)) listener.actionPerformed(ActionEvent(component, ActionEvent.ACTION_PERFORMED, "Enter", keyEvent.`when`, keyEvent.modifiersEx))
} }
@@ -143,5 +165,12 @@ internal open class VimNavigationDispatcher<T : JComponent>(final override val c
action.actionPerformed(actionEvent) 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

@@ -8,6 +8,7 @@ import com.intellij.openapi.ui.getUserData
import com.intellij.openapi.ui.putUserData import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.ui.popup.WizardPopup import com.intellij.ui.popup.WizardPopup
import com.intellij.ui.popup.list.ListPopupImpl
import com.intellij.ui.speedSearch.SpeedSearch import com.intellij.ui.speedSearch.SpeedSearch
import com.intellij.ui.speedSearch.SpeedSearchSupply import com.intellij.ui.speedSearch.SpeedSearchSupply
import java.awt.event.ActionEvent import java.awt.event.ActionEvent
@@ -59,7 +60,7 @@ internal object VimListNavigation {
} }
@Suppress("serial") @Suppress("serial")
private class VimPopupListNavigationDispatcher(component: JList<*>, override val popup: WizardPopup) : VimNavigationDispatcher<JList<*>>(component, POPUP_LIST_ROOT_NODE) { private class VimPopupListNavigationDispatcher(component: JList<*>, override val popup: WizardPopup) : VimNavigationDispatcher<JList<*>>(component, POPUP_LIST_ROOT_NODE, popup.parent) {
init { init {
val speedSearch = SpeedSearchSupply.getSupply(component, true) as? SpeedSearch val speedSearch = SpeedSearchSupply.getSupply(component, true) as? SpeedSearch
if (speedSearch != null) { if (speedSearch != null) {
@@ -85,6 +86,24 @@ internal object VimListNavigation {
// WizardPopup only checks key codes against its input map, but key codes may be undefined for some characters. // 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, 0), pauseAction)
popup.registerAction("KeyboardMaster-VimListNavigation-PauseSpeedSearch", KeyStroke.getKeyStroke(KeyEvent.CHAR_UNDEFINED, KeyEvent.SHIFT_DOWN_MASK), 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() { private class PauseSpeedSearchAction(private val dispatcher: VimNavigationDispatcher<JList<*>>, private val speedSearch: SpeedSearch) : AbstractAction() {

View File

@@ -8,9 +8,12 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.ui.getUserData import com.intellij.openapi.ui.getUserData
import com.intellij.openapi.ui.putUserData import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key import com.intellij.openapi.util.Key
import com.intellij.ui.ClientProperty
import com.intellij.ui.tree.ui.DefaultTreeUI
import java.awt.event.KeyEvent import java.awt.event.KeyEvent
import javax.swing.JTree import javax.swing.JTree
import javax.swing.KeyStroke import javax.swing.KeyStroke
import javax.swing.tree.TreePath
internal object VimTreeNavigation { internal object VimTreeNavigation {
private val KEY = Key.create<VimNavigationDispatcher<JTree>>("KeyboardMaster-VimTreeNavigation") private val KEY = Key.create<VimNavigationDispatcher<JTree>>("KeyboardMaster-VimTreeNavigation")
@@ -22,21 +25,26 @@ internal object VimTreeNavigation {
KeyStroke.getKeyStroke('g') to IdeaAction("Tree-selectFirst"), KeyStroke.getKeyStroke('g') to IdeaAction("Tree-selectFirst"),
KeyStroke.getKeyStroke('j') to SelectLastSibling, KeyStroke.getKeyStroke('j') to SelectLastSibling,
KeyStroke.getKeyStroke('k') to SelectFirstSibling, KeyStroke.getKeyStroke('k') to SelectFirstSibling,
KeyStroke.getKeyStroke('o') to ExpandChildrenToNextLevel,
) )
), ),
KeyStroke.getKeyStroke('G') to IdeaAction("Tree-selectLast"), 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') to IdeaAction("Tree-selectNext"),
KeyStroke.getKeyStroke('j', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectNextSibling"), KeyStroke.getKeyStroke('j', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectNextSibling"),
KeyStroke.getKeyStroke('J') to IdeaAction("Tree-selectNextExtendSelection"), KeyStroke.getKeyStroke('J') to IdeaAction("Tree-selectNextExtendSelection"),
KeyStroke.getKeyStroke('k') to IdeaAction("Tree-selectPrevious"), KeyStroke.getKeyStroke('k') to IdeaAction("Tree-selectPrevious"),
KeyStroke.getKeyStroke('k', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectPreviousSibling"), KeyStroke.getKeyStroke('k', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectPreviousSibling"),
KeyStroke.getKeyStroke('K') to IdeaAction("Tree-selectPreviousExtendSelection"), 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 ExpandOrCollapseTreeNode,
KeyStroke.getKeyStroke('O') to IdeaAction("FullyExpandTreeNode"), KeyStroke.getKeyStroke('O') to IdeaAction("FullyExpandTreeNode"),
KeyStroke.getKeyStroke('p') to IdeaAction("Tree-selectParentNoCollapse"), KeyStroke.getKeyStroke('p') to IdeaAction("Tree-selectParentNoCollapse"),
KeyStroke.getKeyStroke('P') to IdeaAction("Tree-selectFirst"), KeyStroke.getKeyStroke('P') to IdeaAction("Tree-selectFirst"),
KeyStroke.getKeyStroke('x') to CollapseSelfOrParentNode, KeyStroke.getKeyStroke('x') to CollapseSelfOrParentNode,
KeyStroke.getKeyStroke('X') to IdeaAction("CollapseTreeNode"), KeyStroke.getKeyStroke('X') to CollapseAll,
) )
) )
@@ -55,7 +63,51 @@ internal object VimTreeNavigation {
tree.collapsePath(path) tree.collapsePath(path)
} }
else { else {
tree.expandPath(path) 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) }
} }
} }
} }
@@ -66,17 +118,76 @@ internal object VimTreeNavigation {
val path = tree.selectionPath ?: return val path = tree.selectionPath ?: return
if (tree.isExpanded(path)) { if (tree.isExpanded(path)) {
tree.collapsePath(path) collapseAndScroll(tree, path)
} }
else { else {
val parentPath = path.parentPath withParentPath(tree, path) { collapseAndScroll(tree, it) }
if (parentPath.parentPath != null || tree.isRootVisible) {
tree.collapsePath(parentPath)
}
} }
} }
} }
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>> { private data object SelectFirstSibling : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) { override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component val tree = holder.component
@@ -115,8 +226,46 @@ internal object VimTreeNavigation {
} }
} }
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) { private fun selectRow(tree: JTree, row: Int) {
tree.setSelectionRow(row) tree.setSelectionRow(row)
tree.scrollRowToVisible(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)
}
} }