Compare commits

...

7 Commits

13 changed files with 235 additions and 108 deletions

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
/.idea/inspectionProfiles
/.idea/jarRepositories.xml
/.idea/misc.xml
/.idea/*.iml
/.gradle/
/build/

View File

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules">
@ -13,4 +16,4 @@
</GradleProjectSettings>
</option>
</component>
</project>
</project>

View File

@ -0,0 +1,21 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Plugin" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value=":runIde" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@ -0,0 +1,26 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Plugin + IdeaVIM" type="GradleRunConfiguration" factoryName="Gradle">
<ExternalSystemSettings>
<option name="env">
<map>
<entry key="IDEAVIM" value="1" />
</map>
</option>
<option name="executionName" />
<option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="externalSystemIdString" value="GRADLE" />
<option name="scriptParameters" value=":runIde" />
<option name="taskDescriptions">
<list />
</option>
<option name="taskNames">
<list />
</option>
<option name="vmOptions" />
</ExternalSystemSettings>
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
<DebugAllEnabled>false</DebugAllEnabled>
<method v="2" />
</configuration>
</component>

View File

@ -1,11 +1,11 @@
plugins {
kotlin("jvm") version "1.5.10"
id("org.jetbrains.intellij") version "1.1.4"
id("org.jetbrains.intellij") version "1.2.0"
java
}
group = "com.chylex.intellij.keyboardmaster"
version = "0.1.3"
version = "0.1.4"
repositories {
mavenCentral()
@ -18,5 +18,9 @@ dependencies {
}
intellij {
version.set("2021.2")
version.set("2021.2.2")
if (System.getenv("IDEAVIM") == "1") {
plugins.add("IdeaVIM:0.66")
}
}

View File

@ -0,0 +1,24 @@
package com.chylex.intellij.keyboardmaster
import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupKeyHandler
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.startup.StartupActivity
class PluginStartup : StartupActivity.DumbAware {
private var isInitialized = false
override fun runActivity(project: Project) {
if (!isInitialized) {
isInitialized = true
val application = ApplicationManager.getApplication()
if (application.isUnitTestMode) {
CodeCompletionPopupKeyHandler.registerRawHandler()
}
else {
application.invokeLater(CodeCompletionPopupKeyHandler.Companion::registerRawHandler)
}
}
}
}

View File

@ -10,6 +10,7 @@ class PluginConfigurable : Configurable {
private val codeCompletionItemShortcuts = JBTextField(20)
private val codeCompletionNextPageShortcut = JBTextField(2)
private val codeCompletionPrevPageShortcut = JBTextField(2)
override fun getDisplayName(): String {
return "Keyboard Master"
@ -20,6 +21,7 @@ class PluginConfigurable : Configurable {
titledRow("Code Completion") {
row("Item shortcuts:") { component(codeCompletionItemShortcuts) }
row("Next page shortcut:") { component(codeCompletionNextPageShortcut) }
row("Prev page shortcut:") { component(codeCompletionPrevPageShortcut) }
}
}
@ -34,6 +36,7 @@ class PluginConfigurable : Configurable {
PluginConfiguration.modify {
it.codeCompletionItemShortcuts = codeCompletionItemShortcuts.text
it.codeCompletionNextPageShortcut = codeCompletionNextPageShortcut.text.firstOrNull()?.code ?: 0
it.codeCompletionPrevPageShortcut = codeCompletionPrevPageShortcut.text.firstOrNull()?.code ?: 0
}
}
@ -41,6 +44,7 @@ class PluginConfigurable : Configurable {
PluginConfiguration.read {
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() }
}
}
}

View File

@ -1,6 +1,6 @@
package com.chylex.intellij.keyboardmaster.configuration
import com.chylex.intellij.keyboardmaster.lookup.ProjectLookupListener
import com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupConfiguration
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State
@ -14,6 +14,7 @@ import com.intellij.util.xmlb.XmlSerializerUtil
class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
var codeCompletionItemShortcuts = "123456789"
var codeCompletionNextPageShortcut: Int = '0'.code
var codeCompletionPrevPageShortcut: Int = 0
companion object {
private val instance: PluginConfiguration
@ -31,8 +32,8 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
instance.apply(callback).apply(this::update)
}
private fun update(instance: PluginConfiguration) {
ProjectLookupListener.updateShortcuts(instance)
private fun update(instance: PluginConfiguration) = with(instance) {
CodeCompletionPopupConfiguration.updateShortcuts(codeCompletionItemShortcuts, codeCompletionNextPageShortcut, codeCompletionPrevPageShortcut)
}
}

View File

@ -0,0 +1,49 @@
package com.chylex.intellij.keyboardmaster.feature.codeCompletion
import com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration
import com.intellij.util.containers.IntIntHashMap
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 var hintTexts = mutableListOf<String>()
val itemShortcutCount
get() = hintTexts.size
init {
PluginConfiguration.load()
}
fun updateShortcuts(itemShortcutChars: String, nextPageShortcutCode: Int, previousPageShortcutCode: Int) {
charToShortcutMap.clear()
if (nextPageShortcutCode != 0) {
charToShortcutMap[nextPageShortcutCode] = SHORTCUT_NEXT_PAGE
}
if (previousPageShortcutCode != 0) {
charToShortcutMap[previousPageShortcutCode] = SHORTCUT_PREV_PAGE
}
for ((index, char) in itemShortcutChars.withIndex()) {
charToShortcutMap[char.code] = index
}
hintTexts.clear()
for (char in itemShortcutChars) {
hintTexts.add(" [$char]")
}
}
fun getShortcut(char: Char): Int {
return charToShortcutMap[char.code]
}
fun getHintText(index: Int): String {
return hintTexts[index]
}
}

View File

@ -0,0 +1,84 @@
package com.chylex.intellij.keyboardmaster.feature.codeCompletion
import com.intellij.codeInsight.lookup.LookupFocusDegree
import com.intellij.codeInsight.lookup.LookupManager
import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.codeInsight.template.impl.editorActions.TypedActionHandlerBase
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.TypedAction
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.ui.ScrollingUtil
import javax.swing.ListModel
/**
* Handles configured key bindings inside a code completion popup menu.
*/
class CodeCompletionPopupKeyHandler private constructor(originalHandler: TypedActionHandler?) : TypedActionHandlerBase(originalHandler) {
companion object {
/**
* Registers the key handler as a raw handler, because IdeaVIM steals keys from Keyboard Master when renaming an element in normal mode.
*/
fun registerRawHandler() {
TypedAction.getInstance().let { it.setupRawHandler(CodeCompletionPopupKeyHandler(it.rawHandler)) }
}
}
override fun execute(editor: Editor, charTyped: Char, dataContext: DataContext) {
if (!executeImpl(editor, charTyped)) {
myOriginalHandler?.execute(editor, charTyped, dataContext)
}
}
private fun executeImpl(editor: Editor, charTyped: Char): Boolean {
val shortcutItem = CodeCompletionPopupConfiguration.getShortcut(charTyped)
if (shortcutItem == CodeCompletionPopupConfiguration.SHORTCUT_NONE) {
return false
}
val lookup = LookupManager.getActiveLookup(editor)
if (lookup !is LookupImpl) {
return false
}
val offset = CodeCompletionPopupListener.getPageOffset(lookup)
if (shortcutItem == CodeCompletionPopupConfiguration.SHORTCUT_NEXT_PAGE) {
setPageOffset(lookup) {
val newTopIndex = offset + CodeCompletionPopupConfiguration.itemShortcutCount
if (newTopIndex >= it.size) offset else newTopIndex
}
}
else if (shortcutItem == CodeCompletionPopupConfiguration.SHORTCUT_PREV_PAGE) {
setPageOffset(lookup) {
val newTopIndex = offset - CodeCompletionPopupConfiguration.itemShortcutCount
if (newTopIndex < 0) 0 else newTopIndex
}
}
else {
selectItem(lookup, offset + shortcutItem)
}
return true
}
private inline fun setPageOffset(lookup: LookupImpl, getNewTopIndex: (ListModel<*>) -> Int) {
val list = lookup.list
val newTopIndex = getNewTopIndex(list.model)
CodeCompletionPopupListener.setPageOffset(lookup, newTopIndex)
lookup.selectedIndex = newTopIndex
ScrollingUtil.ensureRangeIsVisible(list, newTopIndex, newTopIndex + CodeCompletionPopupConfiguration.itemShortcutCount - 1)
lookup.markSelectionTouched()
lookup.refreshUi(false, true)
}
private fun selectItem(lookup: LookupImpl, index: Int) {
if (!lookup.isFocused) {
lookup.lookupFocusDegree = LookupFocusDegree.FOCUSED
lookup.refreshUi(false, true)
}
lookup.selectedIndex = index
}
}

View File

@ -1,49 +1,20 @@
package com.chylex.intellij.keyboardmaster.lookup
package com.chylex.intellij.keyboardmaster.feature.codeCompletion
import com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration
import com.intellij.codeInsight.lookup.Lookup
import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInsight.lookup.LookupManagerListener
import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.openapi.util.Key
import com.intellij.util.containers.IntIntHashMap
/**
* Adds hints to code completion items with the digit that selects it.
* Adds hints to code completion popup items with the character that selects the item.
*/
class ProjectLookupListener : LookupManagerListener {
class CodeCompletionPopupListener : LookupManagerListener {
companion object {
private val OFFSET_KEY = Key.create<Int>("chylexKeyboardMasterOffset")
private val IS_MODIFIED_KEY = Key.create<Boolean>("chylexKeyboardMasterModified")
private var hintTexts = mutableListOf<String>()
private val charToShortcutMap = IntIntHashMap(16, -1)
val itemShortcutCount
get() = hintTexts.size
init {
PluginConfiguration.load()
}
fun updateShortcuts(configuration: PluginConfiguration) {
hintTexts.clear()
for (char in configuration.codeCompletionItemShortcuts) {
hintTexts.add(" [$char]")
}
charToShortcutMap.clear()
configuration.codeCompletionNextPageShortcut.takeUnless { it == 0 }?.let { charToShortcutMap[it] = 0 }
for ((index, char) in configuration.codeCompletionItemShortcuts.withIndex()) {
charToShortcutMap[char.code] = index + 1
}
}
fun getShortcut(char: Char): Int {
return charToShortcutMap[char.code]
}
fun getLookupOffset(lookup: LookupImpl): Int {
fun getPageOffset(lookup: LookupImpl): Int {
val offset = lookup.getUserData(OFFSET_KEY)
if (offset == null || offset >= lookup.list.model.size) {
return 0
@ -53,13 +24,13 @@ class ProjectLookupListener : LookupManagerListener {
}
}
fun setLookupOffset(lookup: LookupImpl, newOffset: Int) {
fun setPageOffset(lookup: LookupImpl, newOffset: Int) {
lookup.putUserData(OFFSET_KEY, newOffset)
}
}
override fun activeLookupChanged(oldLookup: Lookup?, newLookup: Lookup?) {
if (newLookup !is LookupImpl || newLookup.getUserData(IS_MODIFIED_KEY) == true || itemShortcutCount == 0) {
if (newLookup !is LookupImpl || newLookup.getUserData(IS_MODIFIED_KEY) == true || CodeCompletionPopupConfiguration.itemShortcutCount == 0) {
return
}
@ -69,9 +40,9 @@ class ProjectLookupListener : LookupManagerListener {
newLookup.addPresentationCustomizer { item, presentation ->
val itemList = newLookup.list.model
val itemCount = itemList.size
val offset = getLookupOffset(newLookup)
val offset = getPageOffset(newLookup)
for (index in hintTexts.indices) {
for (index in 0 until CodeCompletionPopupConfiguration.itemShortcutCount) {
val itemIndex = offset + index
if (itemIndex >= itemCount) {
break
@ -80,7 +51,7 @@ class ProjectLookupListener : LookupManagerListener {
if (item === itemList.getElementAt(itemIndex)) {
val customized = LookupElementPresentation()
customized.copyFrom(presentation)
customized.appendTailTextItalic(hintTexts[index], true)
customized.appendTailTextItalic(CodeCompletionPopupConfiguration.getHintText(index), true)
return@addPresentationCustomizer customized
}
}

View File

@ -1,60 +0,0 @@
package com.chylex.intellij.keyboardmaster.lookup
import com.intellij.codeInsight.lookup.LookupFocusDegree
import com.intellij.codeInsight.lookup.LookupManager
import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.codeInsight.template.impl.editorActions.TypedActionHandlerBase
import com.intellij.openapi.actionSystem.DataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.actionSystem.TypedActionHandler
import com.intellij.ui.ScrollingUtil
/**
* When typing digits 1-9 inside a code completion popup menu, selects the n-th item in the list.
* When typing the digit 0, moves down the list by 9 items, wrapping around if needed.
*/
class LookupTypedActionHandler(originalHandler: TypedActionHandler?) : TypedActionHandlerBase(originalHandler) {
override fun execute(editor: Editor, charTyped: Char, dataContext: DataContext) {
if (!executeImpl(editor, charTyped)) {
myOriginalHandler?.execute(editor, charTyped, dataContext)
}
}
private fun executeImpl(editor: Editor, charTyped: Char): Boolean {
val shortcutItem = ProjectLookupListener.getShortcut(charTyped)
if (shortcutItem == -1) {
return false
}
val lookup = LookupManager.getActiveLookup(editor)
if (lookup !is LookupImpl) {
return false
}
val offset = ProjectLookupListener.getLookupOffset(lookup)
if (shortcutItem == 0) {
val list = lookup.list
val itemCount = list.model.size
val shortcutCount = ProjectLookupListener.itemShortcutCount
val topIndex = (offset + shortcutCount).let { if (it >= itemCount) 0 else it }
ProjectLookupListener.setLookupOffset(lookup, topIndex)
lookup.selectedIndex = topIndex
ScrollingUtil.ensureRangeIsVisible(list, topIndex, topIndex + shortcutCount - 1)
lookup.markSelectionTouched()
lookup.refreshUi(false, true)
}
else {
if (!lookup.isFocused) {
lookup.lookupFocusDegree = LookupFocusDegree.FOCUSED
lookup.refreshUi(false, true)
}
lookup.selectedIndex = offset + shortcutItem - 1
}
return true
}
}

View File

@ -13,13 +13,12 @@
<depends>com.intellij.modules.platform</depends>
<projectListeners>
<listener class="com.chylex.intellij.keyboardmaster.lookup.ProjectLookupListener" topic="com.intellij.codeInsight.lookup.LookupManagerListener" />
<listener class="com.chylex.intellij.keyboardmaster.feature.codeCompletion.CodeCompletionPopupListener" topic="com.intellij.codeInsight.lookup.LookupManagerListener" />
</projectListeners>
<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" />
<!--suppress PluginXmlValidity, PluginXmlDynamicPlugin -->
<editorTypedHandler implementationClass="com.chylex.intellij.keyboardmaster.lookup.LookupTypedActionHandler" />
<postStartupActivity implementation="com.chylex.intellij.keyboardmaster.PluginStartup" order="last" />
</extensions>
</idea-plugin>