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/inspectionProfiles
/.idea/jarRepositories.xml /.idea/jarRepositories.xml
/.idea/misc.xml /.idea/misc.xml
/.idea/*.iml
/.gradle/ /.gradle/
/build/ /build/

View File

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings"> <component name="GradleSettings">
<option name="linkedExternalProjectsSettings"> <option name="linkedExternalProjectsSettings">
<GradleProjectSettings> <GradleProjectSettings>
<option name="delegatedBuild" value="true" />
<option name="testRunner" value="GRADLE" />
<option name="distributionType" value="DEFAULT_WRAPPED" /> <option name="distributionType" value="DEFAULT_WRAPPED" />
<option name="externalProjectPath" value="$PROJECT_DIR$" /> <option name="externalProjectPath" value="$PROJECT_DIR$" />
<option name="modules"> <option name="modules">
@ -13,4 +16,4 @@
</GradleProjectSettings> </GradleProjectSettings>
</option> </option>
</component> </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 { plugins {
kotlin("jvm") version "1.5.10" kotlin("jvm") version "1.5.10"
id("org.jetbrains.intellij") version "1.1.4" id("org.jetbrains.intellij") version "1.2.0"
java java
} }
group = "com.chylex.intellij.keyboardmaster" group = "com.chylex.intellij.keyboardmaster"
version = "0.1.3" version = "0.1.4"
repositories { repositories {
mavenCentral() mavenCentral()
@ -18,5 +18,9 @@ dependencies {
} }
intellij { 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 codeCompletionItemShortcuts = JBTextField(20)
private val codeCompletionNextPageShortcut = JBTextField(2) private val codeCompletionNextPageShortcut = JBTextField(2)
private val codeCompletionPrevPageShortcut = JBTextField(2)
override fun getDisplayName(): String { override fun getDisplayName(): String {
return "Keyboard Master" return "Keyboard Master"
@ -20,6 +21,7 @@ class PluginConfigurable : Configurable {
titledRow("Code Completion") { titledRow("Code Completion") {
row("Item shortcuts:") { component(codeCompletionItemShortcuts) } row("Item shortcuts:") { component(codeCompletionItemShortcuts) }
row("Next page shortcut:") { component(codeCompletionNextPageShortcut) } row("Next page shortcut:") { component(codeCompletionNextPageShortcut) }
row("Prev page shortcut:") { component(codeCompletionPrevPageShortcut) }
} }
} }
@ -34,6 +36,7 @@ class PluginConfigurable : Configurable {
PluginConfiguration.modify { PluginConfiguration.modify {
it.codeCompletionItemShortcuts = codeCompletionItemShortcuts.text it.codeCompletionItemShortcuts = codeCompletionItemShortcuts.text
it.codeCompletionNextPageShortcut = codeCompletionNextPageShortcut.text.firstOrNull()?.code ?: 0 it.codeCompletionNextPageShortcut = codeCompletionNextPageShortcut.text.firstOrNull()?.code ?: 0
it.codeCompletionPrevPageShortcut = codeCompletionPrevPageShortcut.text.firstOrNull()?.code ?: 0
} }
} }
@ -41,6 +44,7 @@ class PluginConfigurable : Configurable {
PluginConfiguration.read { PluginConfiguration.read {
codeCompletionItemShortcuts.text = it.codeCompletionItemShortcuts codeCompletionItemShortcuts.text = it.codeCompletionItemShortcuts
codeCompletionNextPageShortcut.text = it.codeCompletionNextPageShortcut.let { code -> if (code == 0) "" else code.toChar().toString() } 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 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.application.ApplicationManager
import com.intellij.openapi.components.PersistentStateComponent import com.intellij.openapi.components.PersistentStateComponent
import com.intellij.openapi.components.State import com.intellij.openapi.components.State
@ -14,6 +14,7 @@ import com.intellij.util.xmlb.XmlSerializerUtil
class PluginConfiguration : PersistentStateComponent<PluginConfiguration> { class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
var codeCompletionItemShortcuts = "123456789" var codeCompletionItemShortcuts = "123456789"
var codeCompletionNextPageShortcut: Int = '0'.code var codeCompletionNextPageShortcut: Int = '0'.code
var codeCompletionPrevPageShortcut: Int = 0
companion object { companion object {
private val instance: PluginConfiguration private val instance: PluginConfiguration
@ -31,8 +32,8 @@ class PluginConfiguration : PersistentStateComponent<PluginConfiguration> {
instance.apply(callback).apply(this::update) instance.apply(callback).apply(this::update)
} }
private fun update(instance: PluginConfiguration) { private fun update(instance: PluginConfiguration) = with(instance) {
ProjectLookupListener.updateShortcuts(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.Lookup
import com.intellij.codeInsight.lookup.LookupElementPresentation import com.intellij.codeInsight.lookup.LookupElementPresentation
import com.intellij.codeInsight.lookup.LookupManagerListener import com.intellij.codeInsight.lookup.LookupManagerListener
import com.intellij.codeInsight.lookup.impl.LookupImpl import com.intellij.codeInsight.lookup.impl.LookupImpl
import com.intellij.openapi.util.Key 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 { companion object {
private val OFFSET_KEY = Key.create<Int>("chylexKeyboardMasterOffset") private val OFFSET_KEY = Key.create<Int>("chylexKeyboardMasterOffset")
private val IS_MODIFIED_KEY = Key.create<Boolean>("chylexKeyboardMasterModified") private val IS_MODIFIED_KEY = Key.create<Boolean>("chylexKeyboardMasterModified")
private var hintTexts = mutableListOf<String>() fun getPageOffset(lookup: LookupImpl): Int {
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 {
val offset = lookup.getUserData(OFFSET_KEY) val offset = lookup.getUserData(OFFSET_KEY)
if (offset == null || offset >= lookup.list.model.size) { if (offset == null || offset >= lookup.list.model.size) {
return 0 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) lookup.putUserData(OFFSET_KEY, newOffset)
} }
} }
override fun activeLookupChanged(oldLookup: Lookup?, newLookup: Lookup?) { 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 return
} }
@ -69,9 +40,9 @@ class ProjectLookupListener : LookupManagerListener {
newLookup.addPresentationCustomizer { item, presentation -> newLookup.addPresentationCustomizer { item, presentation ->
val itemList = newLookup.list.model val itemList = newLookup.list.model
val itemCount = itemList.size 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 val itemIndex = offset + index
if (itemIndex >= itemCount) { if (itemIndex >= itemCount) {
break break
@ -80,7 +51,7 @@ class ProjectLookupListener : LookupManagerListener {
if (item === itemList.getElementAt(itemIndex)) { if (item === itemList.getElementAt(itemIndex)) {
val customized = LookupElementPresentation() val customized = LookupElementPresentation()
customized.copyFrom(presentation) customized.copyFrom(presentation)
customized.appendTailTextItalic(hintTexts[index], true) customized.appendTailTextItalic(CodeCompletionPopupConfiguration.getHintText(index), true)
return@addPresentationCustomizer customized 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> <depends>com.intellij.modules.platform</depends>
<projectListeners> <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> </projectListeners>
<extensions defaultExtensionNs="com.intellij"> <extensions defaultExtensionNs="com.intellij">
<applicationService serviceImplementation="com.chylex.intellij.keyboardmaster.configuration.PluginConfiguration" /> <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" />
<!--suppress PluginXmlValidity, PluginXmlDynamicPlugin --> <postStartupActivity implementation="com.chylex.intellij.keyboardmaster.PluginStartup" order="last" />
<editorTypedHandler implementationClass="com.chylex.intellij.keyboardmaster.lookup.LookupTypedActionHandler" />
</extensions> </extensions>
</idea-plugin> </idea-plugin>