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

Compare commits

...

44 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
ca075869eb Fix vim-style bindings stealing keys from inline text fields 2024-05-17 19:12:08 +02:00
cd69968868 Release version 0.5.4 2024-05-16 16:27:36 +02:00
be41677c59 Fix vim-style bindings forgetting that a search is active 2024-05-16 12:33:27 +02:00
b1d5c57e04 Fix not scrolling selected row into view when jumping to first/last sibling 2024-05-16 12:32:28 +02:00
401d5101e7 Release version 0.5.3 2024-05-06 23:11:29 +02:00
23becff81e Fix ClassCastException in some wrapped Enter action handlers 2024-05-06 23:11:16 +02:00
83bb3f6a83 Add vim-style bindings with selection 2024-05-06 23:05:34 +02:00
e6ab49cb16 Add additional vim-style bindings for navigating submenus 2024-05-06 23:05:33 +02:00
6931387370 Fix vim-style navigation breaking Escape actions 2024-05-06 22:43:39 +02:00
6404b5b926 Release version 0.5.2 2024-05-06 21:03:53 +02:00
2b7f1a363f Fix more cases of vim-style navigation breaking Enter actions 2024-05-06 21:03:40 +02:00
215049bf26 Release version 0.5.1 2024-05-06 05:41:24 +02:00
250e8fa8f8 Fix vim-style navigation breaking Enter actions 2024-05-06 05:40:54 +02:00
381db0e664 Release version 0.5.0 2024-05-05 18:32:52 +02:00
5ca46cf8b2 Implement vim-style navigation for menu popups 2024-05-05 18:32:10 +02:00
d3989a0e87 Implement vim-style navigation for list-based popups 2024-05-05 18:32:09 +02:00
6e38809ef2 Implement vim-style navigation for lists, trees, and tables 2024-05-05 18:32:09 +02:00
085291bf8d Update IDEA to 2024.1.1 2024-05-03 23:54:40 +02:00
a0500a8666 Release version 0.4.1 2023-11-17 08:28:59 +01:00
9953e3b057 Fix binary incompatibility with 2023.3 Beta 2023-11-17 08:28:59 +01:00
15e459d885 Release version 0.4.0 2023-10-19 06:35:35 +02:00
238942c471 Add actions to go to next/previous type in file 2023-10-17 03:38:41 +02:00
6f7cb51300 Add actions to go to next/previous highlighted error in other mode 2023-10-17 02:55:11 +02:00
a3aee8cd84 Release version 0.3.0 2023-10-06 19:51:01 +02:00
40abcbbe6c Fix missing attribute on <applicationConfigurable> tag 2023-10-06 15:59:40 +02:00
d5e96f2205 Update deprecated UI construction code 2023-10-06 15:57:16 +02:00
25 changed files with 1177 additions and 40 deletions

View File

@@ -3,12 +3,12 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm") version "1.8.0"
id("org.jetbrains.intellij") version "1.15.0"
kotlin("jvm") version "1.9.22"
id("org.jetbrains.intellij") version "1.17.3"
}
group = "com.chylex.intellij.keyboardmaster"
version = "0.2.1"
version = "0.6.2"
repositories {
mavenCentral()
@@ -16,11 +16,13 @@ repositories {
intellij {
type.set("IU")
version.set("2023.2")
version.set("2024.2")
updateSinceUntilBuild.set(false)
plugins.add("com.intellij.java")
if (System.getenv("IDEAVIM") == "1") {
plugins.add("IdeaVIM:0.66")
plugins.add("IdeaVIM:2.10.2")
}
}
@@ -29,11 +31,11 @@ kotlin {
}
dependencies {
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0-M1")
}
tasks.patchPluginXml {
sinceBuild.set("232")
sinceBuild.set("242")
}
tasks.test {

View File

@@ -0,0 +1,9 @@
package com.chylex.intellij.keyboardmaster
import com.intellij.openapi.Disposable
import com.intellij.openapi.components.Service
@Service
internal class PluginDisposableService : Disposable {
override fun dispose() {}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
package com.chylex.intellij.keyboardmaster.feature.action.gotoError
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzerSettings
import com.intellij.codeInsight.daemon.impl.GotoNextErrorHandler
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
class GotoErrorInOtherModeHandler(forward: Boolean) : GotoNextErrorHandler(forward) {
override fun invoke(project: Project, editor: Editor, file: PsiFile) {
val settings = DaemonCodeAnalyzerSettings.getInstance()
val oldMode = settings.isNextErrorActionGoesToErrorsFirst
settings.isNextErrorActionGoesToErrorsFirst = !oldMode
try {
super.invoke(project, editor, file)
} finally {
settings.isNextErrorActionGoesToErrorsFirst = oldMode
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,34 @@
package com.chylex.intellij.keyboardmaster.feature.action.gotoType
import com.intellij.codeInsight.actions.BaseCodeInsightAction
import com.intellij.ide.structureView.TreeBasedStructureViewBuilder
import com.intellij.lang.LanguageStructureViewBuilder
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.project.IndexNotReadyException
import com.intellij.openapi.project.Project
import com.intellij.psi.PsiFile
abstract class AbstractGotoTypeInFileAction : BaseCodeInsightAction(), DumbAware {
init {
isEnabledInModalContext = true
}
final override fun isValidForLookup(): Boolean {
return true
}
final override fun isValidForFile(project: Project, editor: Editor, file: PsiFile): Boolean {
return checkValidForFile(file)
}
private companion object {
fun checkValidForFile(file: PsiFile): Boolean {
return try {
LanguageStructureViewBuilder.INSTANCE.getStructureViewBuilder(file) is TreeBasedStructureViewBuilder
} catch (e: IndexNotReadyException) {
false
}
}
}
}

View File

@@ -0,0 +1,9 @@
package com.chylex.intellij.keyboardmaster.feature.action.gotoType
import com.intellij.codeInsight.CodeInsightActionHandler
class GotoNextTypeInFileAction : AbstractGotoTypeInFileAction() {
override fun getHandler(): CodeInsightActionHandler {
return GotoTypeInFileHandler(forward = true)
}
}

View File

@@ -0,0 +1,9 @@
package com.chylex.intellij.keyboardmaster.feature.action.gotoType
import com.intellij.codeInsight.CodeInsightActionHandler
class GotoPreviousTypeInFileAction : AbstractGotoTypeInFileAction() {
override fun getHandler(): CodeInsightActionHandler {
return GotoTypeInFileHandler(forward = false)
}
}

View File

@@ -0,0 +1,97 @@
package com.chylex.intellij.keyboardmaster.feature.action.gotoType
import com.intellij.codeInsight.CodeInsightActionHandler
import com.intellij.codeInsight.lookup.LookupManager
import com.intellij.ide.structureView.StructureViewTreeElement
import com.intellij.ide.structureView.TreeBasedStructureViewBuilder
import com.intellij.lang.LanguageStructureViewBuilder
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.ScrollType
import com.intellij.openapi.fileEditor.ex.IdeDocumentHistory
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.Disposer
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import it.unimi.dsi.fastutil.ints.IntArrayList
class GotoTypeInFileHandler(private val forward: Boolean) : CodeInsightActionHandler {
override fun invoke(project: Project, editor: Editor, file: PsiFile) {
LookupManager.getInstance(project).hideActiveLookup()
val caretOffset = editor.caretModel.offset
val caretLine = editor.caretModel.logicalPosition.line
val searchedOffsetRange = if (forward)
caretOffset + 1..file.textLength
else
0 until caretOffset
val navigationOffsets = getNavigationOffsets(file, searchedOffsetRange)
if (!forward) {
navigationOffsets.reverse()
}
val direction = if (forward) 1 else -1
for (offset in navigationOffsets) {
val line = editor.offsetToLogicalPosition(offset).line
if (line.compareTo(caretLine) * direction > 0) {
editor.caretModel.removeSecondaryCarets()
editor.caretModel.moveToOffset(offset)
editor.selectionModel.removeSelection()
editor.scrollingModel.scrollToCaret(if (forward) ScrollType.CENTER_DOWN else ScrollType.CENTER_UP)
IdeDocumentHistory.getInstance(project).includeCurrentCommandAsNavigation()
break
}
}
}
override fun getElementToMakeWritable(currentFile: PsiFile): PsiElement? {
return null
}
private companion object {
fun getNavigationOffsets(file: PsiFile, searchedOffsetRange: IntRange): IntArray {
val structureViewBuilder = LanguageStructureViewBuilder.getInstance().getStructureViewBuilder(file)
if (structureViewBuilder !is TreeBasedStructureViewBuilder) {
return intArrayOf()
}
val elements = mutableSetOf<PsiElement>()
val model = structureViewBuilder.createStructureViewModel(null)
try {
addStructureViewElements(elements, model.root, file)
} finally {
Disposer.dispose(model)
}
return offsetsFromElements(elements, searchedOffsetRange)
}
private fun addStructureViewElements(result: MutableSet<PsiElement>, parent: StructureViewTreeElement, file: PsiFile) {
for (child in parent.children) {
val value = (child as StructureViewTreeElement).value
if (value is PsiClass && file == value.containingFile) {
result.add(value)
}
addStructureViewElements(result, child, file)
}
}
private fun offsetsFromElements(elements: Collection<PsiElement>, searchedOffsetRange: IntRange): IntArray {
val offsets = IntArrayList(elements.size)
for (element in elements) {
val offset = element.textOffset
if (offset in searchedOffsetRange) {
offsets.add(offset)
}
}
offsets.sort(null)
return offsets.toIntArray()
}
}
}

View File

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

View File

@@ -1,10 +1,13 @@
package com.chylex.intellij.keyboardmaster.feature.codeCompletion
import com.intellij.codeInsight.lookup.Lookup
import com.intellij.codeInsight.lookup.LookupElement
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInsight.lookup.LookupManagerListener
import com.intellij.codeInsight.lookup.impl.LookupCellRenderer.ItemPresentationCustomizer
import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.openapi.util.Key
import javax.swing.Icon
/**
* Adds hints to code completion popup items with the character that selects the item.
@@ -37,26 +40,32 @@ class CodeCompletionPopupListener : LookupManagerListener {
newLookup.putUserData(IS_MODIFIED_KEY, true)
@Suppress("UnstableApiUsage")
newLookup.addPresentationCustomizer { item, presentation ->
val itemList = newLookup.list.model
val itemCount = itemList.size
val offset = getPageOffset(newLookup)
for (index in 0 until CodeCompletionPopupConfiguration.itemShortcutCount) {
val itemIndex = offset + index
if (itemIndex >= itemCount) {
break
newLookup.addPresentationCustomizer(object : ItemPresentationCustomizer {
override fun customizePresentation(item: LookupElement, presentation: LookupElementPresentation): LookupElementPresentation {
val itemList = newLookup.list.model
val itemCount = itemList.size
val offset = getPageOffset(newLookup)
for (index in 0 until CodeCompletionPopupConfiguration.itemShortcutCount) {
val itemIndex = offset + index
if (itemIndex >= itemCount) {
break
}
if (item === itemList.getElementAt(itemIndex)) {
val customized = LookupElementPresentation()
customized.copyFrom(presentation)
customized.appendTailTextItalic(CodeCompletionPopupConfiguration.getHintText(index), true)
return customized
}
}
if (item === itemList.getElementAt(itemIndex)) {
val customized = LookupElementPresentation()
customized.copyFrom(presentation)
customized.appendTailTextItalic(CodeCompletionPopupConfiguration.getHintText(index), true)
return@addPresentationCustomizer customized
}
return presentation
}
presentation
}
override fun customizeEmptyIcon(icon: Icon): Icon {
return icon
}
})
}
}

View File

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

View File

@@ -0,0 +1,92 @@
package com.chylex.intellij.keyboardmaster.feature.vimNavigation
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CustomShortcutSet
import com.intellij.openapi.actionSystem.CustomizedDataContext
import com.intellij.openapi.actionSystem.KeyboardShortcut
import com.intellij.openapi.actionSystem.PlatformDataKeys
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.util.containers.map2Array
import java.awt.event.KeyEvent
import javax.swing.KeyStroke
internal interface KeyStrokeNode<T> {
class Parent<T>(private val keys: Map<KeyStroke, KeyStrokeNode<T>>) : KeyStrokeNode<T> {
val allKeyStrokes: Set<KeyStroke> = mutableSetOf<KeyStroke>().apply {
for ((key, node) in keys) {
add(key)
if (node is Parent) {
addAll(node.allKeyStrokes)
}
}
}
fun getChild(keyEvent: KeyEvent): KeyStrokeNode<T> {
val keyStroke = when {
keyEvent.keyChar != KeyEvent.CHAR_UNDEFINED -> KeyStroke.getKeyStroke(keyEvent.keyChar, keyEvent.modifiersEx and KeyEvent.SHIFT_DOWN_MASK.inv())
keyEvent.id == KeyEvent.KEY_PRESSED -> KeyStroke.getKeyStroke(keyEvent.keyCode, keyEvent.modifiersEx, false)
else -> return this
}
return keys[keyStroke] ?: this
}
operator fun plus(other: Parent<T>): Parent<T> {
val mergedKeys = HashMap(keys)
for ((otherKey, otherNode) in other.keys) {
if (otherNode is Parent) {
val ourNode = keys[otherKey]
if (ourNode is Parent) {
mergedKeys[otherKey] = ourNode + otherNode
continue
}
}
mergedKeys[otherKey] = otherNode
}
return Parent(mergedKeys)
}
}
interface ActionNode<T> : KeyStrokeNode<T> {
fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent)
}
class IdeaAction<T : ComponentHolder>(private val name: String) : ActionNode<T> {
override fun performAction(holder: T, actionEvent: AnActionEvent, keyEvent: KeyEvent) {
val action = actionEvent.actionManager.getAction(name) ?: return
val dataContext = CustomizedDataContext.withProvider(actionEvent.dataContext) {
when {
PlatformDataKeys.CONTEXT_COMPONENT.`is`(it) -> holder.component
else -> null
}
}
ActionUtil.invokeAction(action, dataContext, actionEvent.place, null, null)
}
}
companion object {
fun <T> getAllKeyStrokes(root: Parent<T>, extra: Set<KeyStroke>? = null): Set<KeyStroke> {
val allKeyStrokes = HashSet(root.allKeyStrokes)
if (extra != null) {
allKeyStrokes.addAll(extra)
}
for (c in ('a'..'z') + ('A'..'Z')) {
allKeyStrokes.add(KeyStroke.getKeyStroke(c))
}
return allKeyStrokes
}
fun getAllShortcuts(keyStrokes: Set<KeyStroke>): CustomShortcutSet {
return CustomShortcutSet(*keyStrokes.map2Array { KeyboardShortcut(it, 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 && VimNavigation.isEnabled) {
VimListNavigation.install(component.list, component)
}
return false
}
override fun doIntercept(component: AbstractPopup, owner: RelativePoint?) {}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,15 @@
<idea-plugin>
<actions>
<!-- Go to Type in File -->
<action id="KM.GotoNextTypeInFile"
text="Next Type"
class="com.chylex.intellij.keyboardmaster.feature.action.gotoType.GotoNextTypeInFileAction">
<add-to-group group-id="NavigateInFileGroup" anchor="after" relative-to-action="MethodUp" />
</action>
<action id="KM.GotoPreviousTypeInFile"
text="Previous Type"
class="com.chylex.intellij.keyboardmaster.feature.action.gotoType.GotoPreviousTypeInFileAction">
<add-to-group group-id="NavigateInFileGroup" anchor="after" relative-to-action="KM.GotoNextTypeInFile" />
</action>
</actions>
</idea-plugin>

View File

@@ -7,11 +7,14 @@
Collection of keyboard-centric additions.
<ul>
<li>Code completion items can be jumped to using customizable characters (numbers by default).</li>
<li>Keyboard shortcuts to select next/previous item in focused menu.</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 type in file.</li>
</ul>
]]></description>
<depends>com.intellij.modules.platform</depends>
<depends optional="true" config-file="KeyboardMaster-Java.xml">com.intellij.java</depends>
<projectListeners>
<listener class="com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupListener" topic="com.intellij.codeInsight.lookup.LookupManagerListener" />
@@ -19,7 +22,21 @@
<extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration" />
<applicationConfigurable parentId="tools" instance="com.chylex.intellij.keyboardmaster.configuration.PluginConfigurable" id="com.chylex.keyboardmaster" />
<applicationConfigurable parentId="tools" instance="com.chylex.intellij.keyboardmaster.configuration.PluginConfigurable" id="com.chylex.keyboardmaster" displayName="Keyboard Master" />
<postStartupActivity implementation="com.chylex.intellij.keyboardmaster.PluginStartup" order="last" />
</extensions>
<actions>
<!-- Go to Highlighted Error -->
<action id="KM.GotoNextErrorInOtherMode"
text="Next Highlighted Error in Other Mode"
class="com.chylex.intellij.keyboardmaster.feature.action.gotoError.GotoNextErrorInOtherModeAction">
<add-to-group group-id="GoToErrorGroup" anchor="after" relative-to-action="GotoNextError" />
</action>
<action id="KM.GotoPreviousErrorInOtherMode"
text="Previous Highlighted Error in Other Mode"
class="com.chylex.intellij.keyboardmaster.feature.action.gotoError.GotoPreviousErrorInOtherModeAction">
<add-to-group group-id="GoToErrorGroup" anchor="after" relative-to-action="GotoPreviousError" />
</action>
</actions>
</idea-plugin>