1
0
mirror of https://github.com/chylex/IntelliJ-Keyboard-Master.git synced 2025-12-14 13:28:02 +01:00

Compare commits

..

3 Commits

21 changed files with 234 additions and 754 deletions

1
.gitignore vendored
View File

@@ -2,5 +2,4 @@
!/.idea/runConfigurations !/.idea/runConfigurations
/.gradle/ /.gradle/
/.intellijPlatform/
/build/ /build/

View File

@@ -1,56 +1,49 @@
@file:Suppress("ConvertLambdaToReference") @file:Suppress("ConvertLambdaToReference")
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") kotlin("jvm") version "1.9.22"
id("org.jetbrains.intellij.platform") id("org.jetbrains.intellij") version "1.17.3"
} }
group = "com.chylex.intellij.keyboardmaster" group = "com.chylex.intellij.keyboardmaster"
version = "0.6.6" version = "0.4.1"
repositories { repositories {
mavenCentral() mavenCentral()
intellijPlatform {
defaultRepositories()
}
} }
dependencies { intellij {
intellijPlatform { type.set("IU")
@Suppress("DEPRECATION") version.set("2024.1.1")
intellijIdeaUltimate("2024.2") updateSinceUntilBuild.set(false)
bundledPlugin("com.intellij.java")
if (System.getenv("IDEAVIM") == "1") {
plugin("IdeaVIM", "2.10.2")
}
}
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0-M1") plugins.add("com.intellij.java")
}
if (System.getenv("IDEAVIM") == "1") {
intellijPlatform { plugins.add("IdeaVIM:2.10.2")
pluginConfiguration {
ideaVersion {
sinceBuild.set("242")
untilBuild.set(provider { null })
}
} }
} }
kotlin { kotlin {
jvmToolchain(21) jvmToolchain(17)
}
compilerOptions {
freeCompilerArgs = listOf( dependencies {
"-X" + "jvm-default=all", testImplementation("org.junit.jupiter:junit-jupiter:5.11.0-M1")
"-X" + "lambdas=indy" }
)
} tasks.patchPluginXml {
sinceBuild.set("241")
} }
tasks.test { tasks.test {
useJUnitPlatform() useJUnitPlatform()
} }
tasks.withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs = listOf(
"-Xjvm-default=all"
)
}

Binary file not shown.

View File

@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
networkTimeout=10000 networkTimeout=10000
validateDistributionUrl=true validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME

20
gradlew vendored
View File

@@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
# #
@@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop. # Darwin, MinGW, and NonStop.
# #
# (3) This script is generated from the Groovy template # (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project. # within the Gradle project.
# #
# You can find Gradle at https://github.com/gradle/gradle/. # You can find Gradle at https://github.com/gradle/gradle/.
@@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034 # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/} APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum MAX_FD=maximum
@@ -147,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #( case $MAX_FD in #(
max*) max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
MAX_FD=$( ulimit -H -n ) || MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit" warn "Could not query maximum file descriptor limit"
esac esac
@@ -155,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
'' | soft) :;; #( '' | soft) :;; #(
*) *)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045 # shellcheck disable=SC3045
ulimit -n "$MAX_FD" || ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD" warn "Could not set maximum file descriptor limit to $MAX_FD"
esac esac
@@ -204,11 +202,11 @@ fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command: # Collect all arguments for the java command;
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# and any embedded shellness will be escaped. # shell script including quotes and variable substitutions, so put them in
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # double quotes to make sure that they get re-expanded; and
# treated as '${Hostname}' itself on the command line. # * put everything else in single quotes, so that it's not re-expanded.
set -- \ set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \ "-Dorg.gradle.appname=$APP_BASE_NAME" \

22
gradlew.bat vendored
View File

@@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail
@@ -59,11 +57,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. 1>&2 echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo. 1>&2 echo.
echo Please set the JAVA_HOME variable in your environment to match the 1>&2 echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation. 1>&2 echo location of your Java installation.
goto fail goto fail

View File

@@ -1,8 +1,2 @@
rootProject.name = "KeyboardMaster" rootProject.name = "KeyboardMaster"
pluginManagement {
plugins {
kotlin("jvm") version "1.9.24"
id("org.jetbrains.intellij.platform") version "2.9.0"
}
}

View File

@@ -17,7 +17,7 @@ class PluginConfigurable : Configurable {
private val enableVimNavigation = JBCheckBox("Vim-style navigation in lists / trees / tables").also { checkBox -> private val enableVimNavigation = JBCheckBox("Vim-style navigation in lists / trees / tables").also { checkBox ->
checkBox.addActionListener { checkBox.addActionListener {
if (!checkBox.isSelected) { if (!checkBox.isSelected) {
Messages.showInfoMessage(checkBox, "Vim-style navigation will be fully disabled after restarting the IDE.", "Keyboard Master") Messages.showInfoMessage(checkBox, "Vim-style navigation will be disabled after restarting the IDE.", "Keyboard Master")
} }
} }
} }

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,13 +1,13 @@
package com.chylex.intellij.keyboardmaster.feature.codeCompletion package com.chylex.intellij.keyboardmaster.feature.codeCompletion
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap 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

View File

@@ -17,45 +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 {
isCharEvent(keyEvent) -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv()) keyEvent.keyChar != KeyEvent.CHAR_UNDEFINED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv())
isCodeEvent(keyEvent) -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false) keyEvent.id == KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false)
else -> return this else -> return this
} }
return keys[keyStroke] ?: this return keys[keyStroke] ?: this
} }
private fun isCharEvent(keyEvent: KeyEvent): Boolean { private fun collectKeys(target: MutableSet<KeyStroke>) {
return keyEvent.keyChar != KeyEvent.CHAR_UNDEFINED && (keyEvent.modifiersEx and KeyEvent.CTRL_DOWN_MASK) == 0 for ((key, node) in keys) {
} target.add(key)
private fun isCodeEvent(keyEvent: KeyEvent): Boolean {
return keyEvent.id == KeyEvent.KEY_PRESSED
}
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 if (node is Parent) {
node.collectKeys(target)
}
} }
return Parent(mergedKeys)
} }
} }
@@ -67,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

View File

@@ -0,0 +1,19 @@
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) {
VimListNavigation.install(component.list, component)
}
return false
}
override fun doIntercept(component: AbstractPopup, owner: RelativePoint?) {}
}

View File

@@ -4,17 +4,12 @@ import com.chylex.intellij.keyboardmaster.PluginDisposableService
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimListNavigation 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.VimTableNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTreeNavigation import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimTreeNavigation
import com.intellij.ide.ui.UISettings
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.ui.getUserData import com.intellij.ui.UiInterceptors
import com.intellij.openapi.ui.popup.util.PopupUtil
import com.intellij.openapi.ui.putUserData
import com.intellij.openapi.util.Key
import com.intellij.util.ui.StartupUiUtil import com.intellij.util.ui.StartupUiUtil
import java.awt.AWTEvent import java.awt.AWTEvent
import java.awt.event.FocusEvent import java.awt.event.FocusEvent
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JComponent
import javax.swing.JList import javax.swing.JList
import javax.swing.JTable import javax.swing.JTable
import javax.swing.JTree import javax.swing.JTree
@@ -22,68 +17,53 @@ import javax.swing.KeyStroke
import javax.swing.UIManager import javax.swing.UIManager
object VimNavigation { object VimNavigation {
private val KEY_INSTALLED = Key.create<Boolean>("KeyboardMaster-VimNavigation-Installed") 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(AWTEvent.FOCUS_EVENT_MASK, disposable, ::handleEvent) StartupUiUtil.addAwtListener(::handleEvent, AWTEvent.FOCUS_EVENT_MASK, disposable)
UiInterceptors.registerPersistent(disposable, PopupInterceptor)
} }
fun setEnabled(enabled: Boolean) { fun setEnabled(enabled: Boolean) {
if (!isEnabledFlag.compareAndSet(!enabled, enabled)) { if (!isEnabled.compareAndSet(!enabled, enabled)) {
return return
} }
ApplicationManager.getApplication().invokeLater { if (enabled) {
if (enabled) { val originalBindings = (UIManager.get("PopupMenu.selectedWindowInputMapBindings") as Array<*>).also { originalPopupBindings = it }
val originalBindings = (UIManager.get("PopupMenu.selectedWindowInputMapBindings") as Array<*>).also { originalPopupBindings = it } val updatedBindings = mutableListOf(*originalBindings)
val updatedBindings = mutableListOf(*originalBindings)
updatedBindings.add(KeyStroke.getKeyStroke('h'))
addBinding(updatedBindings, "selectParent", setOf('h', 'p', 'x')) updatedBindings.add("selectParent")
addBinding(updatedBindings, "selectNext", setOf('j'))
addBinding(updatedBindings, "selectPrevious", setOf('k')) updatedBindings.add(KeyStroke.getKeyStroke('j'))
addBinding(updatedBindings, "selectChild", setOf('l', 'o')) updatedBindings.add("selectNext")
addBinding(updatedBindings, "cancel", setOf('q'))
updatedBindings.add(KeyStroke.getKeyStroke('k'))
UIManager.put("PopupMenu.selectedWindowInputMapBindings", updatedBindings.toTypedArray()) updatedBindings.add("selectPrevious")
UISettings.getInstance().disableMnemonics = true
} updatedBindings.add(KeyStroke.getKeyStroke('l'))
else { updatedBindings.add("selectChild")
UIManager.put("PopupMenu.selectedWindowInputMapBindings", originalPopupBindings)
} updatedBindings.add(KeyStroke.getKeyStroke('q'))
updatedBindings.add("cancel")
UIManager.put("PopupMenu.selectedWindowInputMapBindings", updatedBindings.toTypedArray())
} }
} 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) { private fun handleEvent(event: AWTEvent) {
if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED && isEnabled) { if (event is FocusEvent && event.id == FocusEvent.FOCUS_GAINED && isEnabled.get()) {
when (val source = event.source) { when (val source = event.source) {
is JList<*> -> installTo(source, VimListNavigation::install) is JList<*> -> VimListNavigation.install(source)
is JTree -> installTo(source, VimTreeNavigation::install) is JTree -> VimTreeNavigation.install(source)
is JTable -> installTo(source, VimTableNavigation::install) is JTable -> VimTableNavigation.install(source)
}
}
}
private inline fun <T : JComponent> installTo(component: T, installer: (T) -> Unit) {
if (component.getUserData(KEY_INSTALLED) == null) {
component.putUserData(KEY_INSTALLED, true)
if (PopupUtil.getPopupContainerFor(component) == null) {
installer(component)
} }
} }
} }

View File

@@ -1,104 +1,56 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.WrappedAction.ForKeyListener import com.chylex.intellij.keyboardmaster.PluginDisposableService
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.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.actionSystem.KeyboardShortcut import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.actionSystem.PlatformCoreDataKeys
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.openapi.wm.ex.ToolWindowManagerEx
import com.intellij.pom.Navigatable
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
import com.intellij.ui.speedSearch.SpeedSearchActivator
import com.intellij.ui.speedSearch.SpeedSearchSupply 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.awt.event.KeyEvent
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
import javax.swing.text.JTextComponent
internal open class VimNavigationDispatcher<T : JComponent>(final override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>, disposable: Disposable? = null) : DumbAwareAction(), ComponentHolder { internal open class VimNavigationDispatcher<T : JComponent>(final override val component: T, private val rootNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>>) : DumbAwareAction(), ComponentHolder {
companion object { private companion object {
@JvmStatic private val DISPOSABLE = ApplicationManager.getApplication().getService(PluginDisposableService::class.java)
protected val ENTER_KEY: KeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0) private val EXTRA_SHORTCUTS = setOf(
KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0),
private val CTRL_ENTER_KEY: KeyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, KeyEvent.CTRL_DOWN_MASK) KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
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 private var currentNode: KeyStrokeNode.Parent<VimNavigationDispatcher<T>> = rootNode
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)
val speedSearch = SpeedSearchSupply.getSupply(component, true)
speedSearch?.addChangeListener {
if (it.propertyName == SpeedSearchSupply.ENTERED_PREFIX_PROPERTY_NAME && !speedSearch.isPopupActive) {
isSearching.set(false)
}
}
} }
protected fun getAllKeyStrokes(): Set<KeyStroke> { protected fun getAllKeyStrokes(): Set<KeyStroke> {
return KeyStrokeNode.getAllKeyStrokes(rootNode, setOf(ENTER_KEY, CTRL_ENTER_KEY, META_ENTER_KEY)) return KeyStrokeNode.getAllKeyStrokes(rootNode, EXTRA_SHORTCUTS)
}
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) { final override fun actionPerformed(e: AnActionEvent) {
val keyEvent = e.inputEvent as? KeyEvent ?: return val keyEvent = e.inputEvent as? KeyEvent ?: return
if (keyEvent.id == KeyEvent.KEY_PRESSED && keyEvent.keyCode == KeyEvent.VK_ENTER) { if (keyEvent.id == KeyEvent.KEY_PRESSED && handleSpecialKeyPress(keyEvent, e.dataContext)) {
handleEnterKeyPress(e, keyEvent) currentNode = rootNode
return return
} }
@@ -111,73 +63,65 @@ internal open class VimNavigationDispatcher<T : JComponent>(final override val c
} }
} }
private fun handleEnterKeyPress(actionEvent: AnActionEvent, keyEvent: KeyEvent) { private fun handleSpecialKeyPress(keyEvent: KeyEvent, dataContext: DataContext): Boolean {
handleEnterKeyPress(keyEvent) { originalEnterAction.perform(actionEvent, it) } if (keyEvent.keyCode == KeyEvent.VK_ESCAPE) {
return true
}
if (keyEvent.keyCode == KeyEvent.VK_ENTER) {
handleEnterKeyPress(dataContext)
return true
}
return false
} }
protected inline fun handleEnterKeyPress(keyEvent: KeyEvent, originalAction: (KeyEvent) -> Unit) { private fun handleEnterKeyPress(dataContext: DataContext) {
if (isSearching.compareAndSet(true, false) && !runEnterActionImmediately(keyEvent)) { if (isSearching.compareAndSet(true, false)) {
stopSpeedSearch() when (val supply = SpeedSearchSupply.getSupply(component)) {
is SpeedSearchBase<*> -> supply.hidePopup()
is SpeedSearch -> supply.reset()
}
} }
else { else {
currentNode = rootNode val navigatables = dataContext.getData(CommonDataKeys.NAVIGATABLE_ARRAY)?.filter(Navigatable::canNavigate).orEmpty()
originalAction(keyEvent) for ((index, navigatable) in navigatables.withIndex()) {
} navigatable.navigate(index == navigatables.lastIndex)
} }
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) { final override fun update(e: AnActionEvent) {
if (!isSearching.get() && SpeedSearchSupply.getSupply(component)?.isPopupActive == true) { e.presentation.isEnabled = !isSearching.get() || e.inputEvent.let { it is KeyEvent && it.id == KeyEvent.KEY_PRESSED && it.keyCode == KeyEvent.VK_ENTER }
isSearching.set(true)
}
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.getData(CommonDataKeys.EDITOR) != null
|| e.getData(PlatformCoreDataKeys.CONTEXT_COMPONENT) is JTextComponent
} }
final override fun getActionUpdateThread(): ActionUpdateThread { final override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.EDT return ActionUpdateThread.BGT
} }
private sealed interface WrappedAction { class StartSearch<T : JComponent> : KeyStrokeNode.ActionNode<VimNavigationDispatcher<T>> {
fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) @Suppress("UnstableApiUsage")
override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
class ForActionListener(private val component: JComponent, private val listener: ActionListener) : WrappedAction { val speedSearch = SpeedSearchSupply.getSupply(holder.component, true) as? SpeedSearchActivator ?: return
override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) { if (speedSearch.isAvailable) {
listener.actionPerformed(ActionEvent(component, ActionEvent.ACTION_PERFORMED, "Enter", keyEvent.`when`, keyEvent.modifiersEx)) holder.isSearching.set(true)
speedSearch.activate()
} }
} }
}
class ForAnAction(val action: AnAction) : WrappedAction {
override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) { @Suppress("UnstableApiUsage")
action.actionPerformed(actionEvent) class CloseParentPopupOrToolWindow<T : JComponent> : KeyStrokeNode.ActionNode<VimNavigationDispatcher<T>> {
} override fun performAction(holder: VimNavigationDispatcher<T>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
} val popup = holder.popup
if (popup != null) {
class ForKeyListener(private val component: JComponent) : WrappedAction { popup.cancel()
override fun perform(actionEvent: AnActionEvent, keyEvent: KeyEvent) { return
val unconsumedKeyEvent = KeyEvent(component, keyEvent.id, keyEvent.`when`, keyEvent.modifiersEx, keyEvent.keyCode, keyEvent.keyChar, keyEvent.keyLocation)
component.dispatchEvent(unconsumedKeyEvent)
} }
val project = actionEvent.project ?: return
val decorator = InternalDecoratorImpl.findNearestDecorator(holder.component) ?: return
ToolWindowManagerEx.getInstanceEx(project).hideToolWindow(decorator.toolWindowId, true)
} }
} }
} }

View File

@@ -1,98 +0,0 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode
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.ComponentUtil
import com.intellij.ui.speedSearch.SpeedSearchActivator
import com.intellij.ui.speedSearch.SpeedSearchSupply
import java.awt.Point
import java.awt.event.KeyEvent
import javax.swing.JComponent
import javax.swing.JScrollPane
import javax.swing.JViewport
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(),
)
)
inline fun <T> withShiftModifier(keyCode: Int, modifiers: Int, constructor: (Boolean) -> KeyStrokeNode<T>): Array<Pair<KeyStroke, KeyStrokeNode<T>>> {
return arrayOf(
KeyStroke.getKeyStroke(keyCode, modifiers) to constructor(false),
KeyStroke.getKeyStroke(keyCode, modifiers or KeyEvent.SHIFT_DOWN_MASK) to constructor(true),
)
}
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)
}
}
}
fun JComponent.findScrollPane(): JScrollPane? {
return ComponentUtil.getParentOfType(JScrollPane::class.java, this)
}
fun JScrollPane.scrollByPages(pages: Float) {
scrollBy(this) { (it.height.toFloat() * pages).toInt() }
}
fun JScrollPane.scrollBy(amount: Int) {
scrollBy(this) { amount }
}
private inline fun scrollBy(scrollPane: JScrollPane, amount: (JViewport) -> Int) {
val viewport = scrollPane.viewport
val position = viewport.viewPosition
val scrollTo = (position.y + amount(viewport)).coerceIn(0, viewport.viewSize.height - viewport.height)
viewport.viewPosition = Point(position.x, scrollTo)
}
}

View File

@@ -1,20 +1,15 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components 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.IdeaAction
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent 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
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.findScrollPane import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.CloseParentPopupOrToolWindow
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.scrollBy import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.StartSearch
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.scrollByPages
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.withShiftModifier
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
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.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
@@ -26,30 +21,23 @@ import javax.swing.KeyStroke
internal object VimListNavigation { internal object VimListNavigation {
private val KEY = Key.create<VimNavigationDispatcher<JList<*>>>("KeyboardMaster-VimListNavigation") private val KEY = Key.create<VimNavigationDispatcher<JList<*>>>("KeyboardMaster-VimListNavigation")
private val ROOT_NODE = VimCommonNavigation.commonRootNode<JList<*>>() + Parent( private val ROOT_NODE = Parent<VimNavigationDispatcher<JList<*>>>(
mapOf( mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("List-selectFirstRow"), KeyStroke.getKeyStroke('A') to IdeaAction("MaximizeToolWindow"),
KeyStroke.getKeyStroke('f') to StartSearch(),
KeyStroke.getKeyStroke('g') to Parent(
mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("List-selectFirstRow"),
)
),
KeyStroke.getKeyStroke('G') to IdeaAction("List-selectLastRow"), KeyStroke.getKeyStroke('G') to IdeaAction("List-selectLastRow"),
KeyStroke.getKeyStroke('h') to IdeaAction("List-selectPreviousColumn"), 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-selectNextRow"),
KeyStroke.getKeyStroke('J') to IdeaAction("List-selectNextRowExtendSelection"),
KeyStroke.getKeyStroke('k') to IdeaAction("List-selectPreviousRow"), 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-selectNextColumn"),
KeyStroke.getKeyStroke('L') to IdeaAction("List-selectNextColumnExtendSelection"), KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"),
*withShiftModifier(KeyEvent.VK_B, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = -1.0F, extendSelection = it) }, KeyStroke.getKeyStroke('q') to CloseParentPopupOrToolWindow(),
*withShiftModifier(KeyEvent.VK_D, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = +0.5F, extendSelection = it) }, KeyStroke.getKeyStroke('/') to StartSearch(),
*withShiftModifier(KeyEvent.VK_F, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = +1.0F, extendSelection = it) },
*withShiftModifier(KeyEvent.VK_U, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = -0.5F, extendSelection = it) },
)
)
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"),
) )
) )
@@ -65,37 +53,8 @@ internal object VimListNavigation {
} }
} }
private data class ScrollVerticallyAndSelect(private val pages: Float, private val extendSelection: Boolean) : ActionNode<VimNavigationDispatcher<JList<*>>> {
override fun performAction(holder: VimNavigationDispatcher<JList<*>>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val list = holder.component
val scrollPane = list.findScrollPane() ?: return
scrollPane.scrollByPages(pages)
if (pages < 0F) {
val topItemBounds = list.firstVisibleIndex.let { list.getCellBounds(it, it) } ?: return
scrollPane.scrollBy(topItemBounds.height - 1)
}
val topItemIndex = list.firstVisibleIndex
if (topItemIndex == -1) {
return
}
if (extendSelection) {
val anchor = list.anchorSelectionIndex.takeIf { it in 0 until list.model.size } ?: 0
list.setSelectionInterval(anchor, topItemIndex)
}
else {
list.selectedIndex = topItemIndex
}
list.ensureIndexIsVisible(topItemIndex)
}
}
@Suppress("serial") @Suppress("serial")
private class VimPopupListNavigationDispatcher(component: JList<*>, override val popup: WizardPopup) : VimNavigationDispatcher<JList<*>>(component, POPUP_LIST_ROOT_NODE, popup.parent) { private class VimPopupListNavigationDispatcher(component: JList<*>, override val popup: WizardPopup) : VimNavigationDispatcher<JList<*>>(component, ROOT_NODE) {
init { init {
val speedSearch = SpeedSearchSupply.getSupply(component, true) as? SpeedSearch val speedSearch = SpeedSearchSupply.getSupply(component, true) as? SpeedSearch
if (speedSearch != null) { if (speedSearch != null) {
@@ -121,24 +80,6 @@ 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

@@ -1,41 +1,36 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation.components 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.IdeaAction
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Parent 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
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.findScrollPane import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.CloseParentPopupOrToolWindow
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.scrollBy import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.StartSearch
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.scrollByPages
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.withShiftModifier
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 java.awt.Rectangle
import java.awt.event.KeyEvent
import javax.swing.JTable import javax.swing.JTable
import javax.swing.KeyStroke import javax.swing.KeyStroke
internal object VimTableNavigation { internal object VimTableNavigation {
private val KEY = Key.create<VimNavigationDispatcher<JTable>>("KeyboardMaster-VimTableNavigation") private val KEY = Key.create<VimNavigationDispatcher<JTable>>("KeyboardMaster-VimTableNavigation")
private val ROOT_NODE = VimCommonNavigation.commonRootNode<JTable>() + Parent( private val ROOT_NODE = Parent<VimNavigationDispatcher<JTable>>(
mapOf( mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("Table-selectFirstRow"), KeyStroke.getKeyStroke('A') to IdeaAction("MaximizeToolWindow"),
KeyStroke.getKeyStroke('f') to StartSearch(),
KeyStroke.getKeyStroke('g') to Parent(
mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("Table-selectFirstRow"),
)
),
KeyStroke.getKeyStroke('G') to IdeaAction("Table-selectLastRow"), KeyStroke.getKeyStroke('G') to IdeaAction("Table-selectLastRow"),
KeyStroke.getKeyStroke('h') to IdeaAction("Table-selectPreviousColumn"), 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-selectNextRow"),
KeyStroke.getKeyStroke('J') to IdeaAction("Table-selectNextRowExtendSelection"),
KeyStroke.getKeyStroke('k') to IdeaAction("Table-selectPreviousRow"), 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-selectNextColumn"),
KeyStroke.getKeyStroke('L') to IdeaAction("Table-selectNextColumnExtendSelection"), KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"),
*withShiftModifier(KeyEvent.VK_B, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = -1.0F, extendSelection = it) }, KeyStroke.getKeyStroke('q') to CloseParentPopupOrToolWindow(),
*withShiftModifier(KeyEvent.VK_D, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = +0.5F, extendSelection = it) }, KeyStroke.getKeyStroke('/') to StartSearch(),
*withShiftModifier(KeyEvent.VK_F, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = +1.0F, extendSelection = it) },
*withShiftModifier(KeyEvent.VK_U, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = -0.5F, extendSelection = it) },
) )
) )
@@ -44,29 +39,4 @@ internal object VimTableNavigation {
component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE)) component.putUserData(KEY, VimNavigationDispatcher(component, ROOT_NODE))
} }
} }
private data class ScrollVerticallyAndSelect(private val pages: Float, private val extendSelection: Boolean) : ActionNode<VimNavigationDispatcher<JTable>> {
override fun performAction(holder: VimNavigationDispatcher<JTable>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val table = holder.component
val scrollPane = table.findScrollPane() ?: return
scrollPane.scrollByPages(pages)
if (pages < 0F) {
scrollPane.scrollBy(table.rowHeight - 1)
}
val visibleRect = table.visibleRect
val rowIndexToSelect = table.rowAtPoint(visibleRect.location)
if (rowIndexToSelect == -1) {
return
}
val rowRect = table.getCellRect(rowIndexToSelect, 0, true)
val adjustedRect = Rectangle(visibleRect.x, rowRect.y, visibleRect.width, visibleRect.height)
table.changeSelection(rowIndexToSelect, table.selectedColumn, false, extendSelection)
table.scrollRectToVisible(adjustedRect)
}
}
} }

View File

@@ -4,61 +4,43 @@ import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.Ac
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.KeyStrokeNode.IdeaAction 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.KeyStrokeNode.Parent
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.findScrollPane import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.CloseParentPopupOrToolWindow
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.scrollBy import com.chylex.intellij.keyboardmaster.feature.vimNavigation.VimNavigationDispatcher.StartSearch
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.scrollByPages
import com.chylex.intellij.keyboardmaster.feature.vimNavigation.components.VimCommonNavigation.withShiftModifier
import com.intellij.openapi.actionSystem.AnActionEvent 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.TreeModel
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")
private val ROOT_NODE = VimCommonNavigation.commonRootNode<JTree>() + Parent( private val ROOT_NODE = Parent(
mapOf( mapOf(
KeyStroke.getKeyStroke('a') to Parent( KeyStroke.getKeyStroke('A') to IdeaAction("MaximizeToolWindow"),
mapOf( KeyStroke.getKeyStroke('f') to StartSearch(),
KeyStroke.getKeyStroke('o') to ExpandAll,
KeyStroke.getKeyStroke('x') to CollapseAll,
)
),
KeyStroke.getKeyStroke('g') to Parent( KeyStroke.getKeyStroke('g') to Parent(
mapOf( mapOf(
KeyStroke.getKeyStroke('g') to IdeaAction("Tree-selectFirst"), KeyStroke.getKeyStroke('g') to IdeaAction("Tree-selectFirst"),
KeyStroke.getKeyStroke('j') to IdeaAction("Tree-selectNextSibling"),
KeyStroke.getKeyStroke('k') to IdeaAction("Tree-selectPreviousSibling"),
) )
), ),
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') to IdeaAction("Tree-selectNextExtendSelection"), 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') to IdeaAction("Tree-selectPrevious"),
KeyStroke.getKeyStroke('K') to IdeaAction("Tree-selectPreviousExtendSelection"), KeyStroke.getKeyStroke('k', KeyEvent.ALT_DOWN_MASK) to IdeaAction("Tree-selectPreviousSibling"),
KeyStroke.getKeyStroke('l') to ExpandSelfOrMoveToFirstChildNode, KeyStroke.getKeyStroke('K') to SelectFirstSibling,
KeyStroke.getKeyStroke('L') to ExpandUntilFirstLeafNode, KeyStroke.getKeyStroke('m') to IdeaAction("ShowPopupMenu"),
KeyStroke.getKeyStroke('o') to ExpandChildrenToNextLevel, 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('q') to CloseParentPopupOrToolWindow(),
KeyStroke.getKeyStroke('s') to SelectFirstSibling, KeyStroke.getKeyStroke('x') to CollapseSelfOrParentNode,
KeyStroke.getKeyStroke('S') to SelectLastSibling, KeyStroke.getKeyStroke('X') to IdeaAction("CollapseTreeNode"),
KeyStroke.getKeyStroke('x') to CollapseChildrenToPreviousLevel, KeyStroke.getKeyStroke('/') to StartSearch(),
KeyStroke.getKeyStroke('X') to CollapseSelf,
*withShiftModifier(KeyEvent.VK_B, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = -1.0F, extendSelection = it) },
*withShiftModifier(KeyEvent.VK_D, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = +0.5F, extendSelection = it) },
*withShiftModifier(KeyEvent.VK_F, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = +1.0F, extendSelection = it) },
*withShiftModifier(KeyEvent.VK_U, KeyEvent.CTRL_DOWN_MASK) { ScrollVerticallyAndSelect(pages = -0.5F, extendSelection = it) },
) )
) )
@@ -68,207 +50,37 @@ internal object VimTreeNavigation {
} }
} }
private data class ScrollVerticallyAndSelect(private val pages: Float, private val extendSelection: Boolean) : ActionNode<VimNavigationDispatcher<JTree>> { private data object ExpandOrCollapseTreeNode : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val scrollPane = tree.findScrollPane() ?: return
scrollPane.scrollByPages(pages)
if (pages < 0F) {
val topPath = pathOnTop(tree) ?: return
val topPathBounds = tree.getPathBounds(topPath) ?: return
scrollPane.scrollBy(topPathBounds.height - 1)
}
val pathToSelect = pathOnTop(tree) ?: return
if (extendSelection) {
val anchor = tree.anchorSelectionPath
val anchorRow = if (anchor == null) -1 else tree.getRowForPath(anchor)
if (anchorRow < 0) {
tree.selectionPath = pathToSelect
}
else {
tree.setSelectionInterval(tree.getRowForPath(pathToSelect), anchorRow)
tree.setAnchorSelectionPath(anchor)
tree.leadSelectionPath = pathToSelect
}
}
else {
tree.selectionPath = pathToSelect
}
tree.scrollRectToVisible(tree.getPathBounds(pathToSelect))
}
private fun pathOnTop(tree: JTree): TreePath? {
return tree.visibleRect.let { tree.getClosestPathForLocation(it.x, it.y) }
}
}
private data object CollapseSelf : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
val path = tree.selectionPath ?: return
collapseAndScroll(tree, path)
}
}
private data object CollapseSelfOrMoveToParentNode : 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
val path = tree.selectionPath ?: return val path = tree.selectionPath ?: return
if (tree.isExpanded(path)) { if (tree.isExpanded(path)) {
collapseAndScroll(tree, path) tree.collapsePath(path)
} }
else { else {
withParentPath(tree, path) { selectRow(tree, it) } tree.expandPath(path)
} }
} }
} }
private data object CollapseUntilRootNode : ActionNode<VimNavigationDispatcher<JTree>> { private data object CollapseSelfOrParentNode : 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
val path = tree.selectionPath ?: return 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
var row = 0
while (row < tree.rowCount) {
tree.collapseRow(row)
row++
}
}
}
private data object CollapseChildrenToPreviousLevel : 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?.takeIf(tree::isExpanded) ?: return
var currentLevel = mutableListOf(path)
while (true) {
val nextLevel = mutableListOf<TreePath>()
for (parentPath in currentLevel) {
forEachChild(model, parentPath) {
val childPath = parentPath.pathByAddingChild(it)
if (tree.isExpanded(childPath)) {
nextLevel.add(childPath)
}
}
}
if (nextLevel.isNotEmpty()) {
currentLevel = nextLevel
}
else {
break
}
}
for (parentPath in currentLevel) {
tree.collapsePath(parentPath)
}
}
}
private data object ExpandAll : ActionNode<VimNavigationDispatcher<JTree>> {
override fun performAction(holder: VimNavigationDispatcher<JTree>, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val tree = holder.component
var row = 0
while (row < tree.rowCount) {
tree.expandRow(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) }
for (path in pathsToExpand) {
forEachChild(model, path) { tree.collapsePath(path.pathByAddingChild(it)) }
}
break
}
val nextPathsToExpand = mutableListOf<TreePath>()
for (parentPath in pathsToExpand) {
forEachChild(model, parentPath) {
if (!model.isLeaf(it)) {
nextPathsToExpand.add(parentPath.pathByAddingChild(it))
}
}
}
pathsToExpand = nextPathsToExpand
} while (pathsToExpand.isNotEmpty())
}
}
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)) { if (tree.isExpanded(path)) {
selectRow(tree, getFirstChild(tree, path)) tree.collapsePath(path)
} }
else { else {
runWithoutAutoExpand(tree) { tree.expandPath(path) } val parentPath = path.parentPath
if (parentPath.parentPath != null || tree.isRootVisible) {
tree.collapsePath(parentPath)
}
} }
} }
} }
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 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
@@ -277,7 +89,7 @@ internal object VimTreeNavigation {
val parentPath = path.parentPath ?: return val parentPath = path.parentPath ?: return
val parentRow = tree.getRowForPath(parentPath) val parentRow = tree.getRowForPath(parentPath)
selectRow(tree, parentRow + 1) tree.setSelectionRow(parentRow + 1)
} }
} }
@@ -303,58 +115,7 @@ internal object VimTreeNavigation {
} }
} }
selectRow(tree, targetRow) tree.setSelectionRow(targetRow)
} }
} }
private inline fun forEachChild(model: TreeModel, path: TreePath, action: (Any) -> Unit) {
val lastPathComponent = path.lastPathComponent
for (i in 0 until model.getChildCount(lastPathComponent)) {
action(model.getChild(lastPathComponent, i))
}
}
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.scrollPathToVisible(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>