1
0
mirror of https://github.com/chylex/IntelliJ-AceJump.git synced 2025-09-15 14:32:08 +02:00

4 Commits

10 changed files with 117 additions and 93 deletions

View File

@@ -18,12 +18,12 @@ intellij {
version.set("2024.2")
updateSinceUntilBuild.set(false)
// plugins.add("IdeaVIM:chylex-40")
// plugins.add("com.intellij.classic.ui:242.20224.159")
plugins.add("IdeaVIM:chylex-40")
plugins.add("com.intellij.classic.ui:242.20224.159")
pluginsRepositories {
// custom("https://intellij.chylex.com")
// marketplace()
custom("https://intellij.chylex.com")
marketplace()
}
}

View File

@@ -1,5 +1,6 @@
package org.acejump.action
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager
@@ -167,6 +168,10 @@ sealed class AceVimAction : DumbAwareAction() {
action.presentation.isEnabled = action.getData(CommonDataKeys.EDITOR) != null
}
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR) ?: return
val session = SessionManager.start(editor, AceVimMode.JumpAllEditors.getJumpEditors(editor))

View File

@@ -13,7 +13,6 @@ class AceConfigurable : Configurable {
override fun isModified() =
panel.allowedChars != settings.allowedChars ||
panel.prefixChars != settings.prefixChars ||
panel.keyboardLayout != settings.layout ||
panel.minQueryLengthInt != settings.minQueryLength ||
panel.editorFadeOpacityPercent != settings.editorFadeOpacity ||
@@ -24,7 +23,6 @@ class AceConfigurable : Configurable {
override fun apply() {
settings.allowedChars = panel.allowedChars
settings.prefixChars = panel.prefixChars
settings.layout = panel.keyboardLayout
settings.minQueryLength = panel.minQueryLengthInt ?: settings.minQueryLength
settings.editorFadeOpacity = panel.editorFadeOpacityPercent

View File

@@ -8,7 +8,6 @@ import java.awt.Color
data class AceSettings(
var layout: KeyLayout = QWERTY,
var allowedChars: String = layout.allChars,
var prefixChars: String = ";",
var minQueryLength: Int = 1,
var editorFadeOpacity: Int = 70,

View File

@@ -5,16 +5,15 @@ import com.intellij.ui.ColorPanel
import com.intellij.ui.components.JBSlider
import com.intellij.ui.components.JBTextArea
import com.intellij.ui.components.JBTextField
import com.intellij.ui.layout.Cell
import com.intellij.ui.layout.GrowPolicy.MEDIUM_TEXT
import com.intellij.ui.layout.GrowPolicy.SHORT_TEXT
import com.intellij.ui.layout.panel
import com.intellij.ui.dsl.builder.COLUMNS_LARGE
import com.intellij.ui.dsl.builder.COLUMNS_SHORT
import com.intellij.ui.dsl.builder.columns
import com.intellij.ui.dsl.builder.panel
import org.acejump.input.KeyLayout
import java.awt.Color
import java.awt.Font
import java.util.Hashtable
import javax.swing.JCheckBox
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
import javax.swing.JSlider
@@ -27,7 +26,6 @@ import kotlin.reflect.KProperty
@Suppress("UsePropertyAccessSyntax")
internal class AceSettingsPanel {
private val tagAllowedCharsField = JBTextField()
private val tagPrefixCharsField = JBTextField()
private val keyboardLayoutCombo = ComboBox<KeyLayout>()
private val keyboardLayoutArea = JBTextArea().apply { isEditable = false }
private val minQueryLengthField = JBTextField()
@@ -46,52 +44,40 @@ internal class AceSettingsPanel {
init {
tagAllowedCharsField.apply { font = Font("monospaced", font.style, font.size) }
tagPrefixCharsField.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutArea.apply { font = Font("monospaced", font.style, font.size) }
keyboardLayoutCombo.setupEnumItems { keyChars = it.rows.joinToString("\n") }
}
internal val rootPanel: JPanel = panel {
fun Cell.short(component: JComponent) = component(growPolicy = SHORT_TEXT)
fun Cell.medium(component: JComponent) = component(growPolicy = MEDIUM_TEXT)
titledRow("Characters and Layout") {
row("Allowed characters in tags:") { medium(tagAllowedCharsField) }
row("Allowed prefix characters in tags:") { medium(tagPrefixCharsField) }
row("Keyboard layout:") { short(keyboardLayoutCombo) }
row("Keyboard design:") { short(keyboardLayoutArea) }
group("Characters and Layout") {
row("Allowed characters in tags:") { cell(tagAllowedCharsField).columns(COLUMNS_LARGE) }
row("Keyboard layout:") { cell(keyboardLayoutCombo).columns(COLUMNS_SHORT) }
row("Keyboard design:") { cell(keyboardLayoutArea).columns(COLUMNS_SHORT) }
}
titledRow("Behavior") {
row("Minimum typed characters (1-10):") { short(minQueryLengthField) }
group("Behavior") {
row("Minimum typed characters (1-10):") { cell(minQueryLengthField).columns(COLUMNS_SHORT) }
}
titledRow("Colors") {
group("Colors") {
row("Caret background:") {
cell {
component(jumpModeColorWheel)
}
cell(jumpModeColorWheel)
}
row("Tag foreground:") {
cell {
component(tagForeground1ColorWheel)
component(tagForeground2ColorWheel)
}
cell(tagForeground1ColorWheel)
cell(tagForeground2ColorWheel)
}
row("Search highlight:") {
cell {
component(searchHighlightColorWheel)
}
cell(searchHighlightColorWheel)
}
row("Editor fade opacity (%):") {
medium(editorFadeOpacitySlider)
cell(editorFadeOpacitySlider)
}
}
}
// Property-to-property delegation: https://stackoverflow.com/q/45074596/1772342
internal var allowedChars by tagAllowedCharsField
internal var prefixChars by tagPrefixCharsField
internal var keyboardLayout by keyboardLayoutCombo
internal var keyChars by keyboardLayoutArea
internal var minQueryLength by minQueryLengthField
@@ -111,7 +97,6 @@ internal class AceSettingsPanel {
fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars
prefixChars = settings.prefixChars
keyboardLayout = settings.layout
minQueryLength = settings.minQueryLength.toString()
editorFadeOpacityPercent = settings.editorFadeOpacity
@@ -123,7 +108,7 @@ internal class AceSettingsPanel {
// Removal pending support for https://youtrack.jetbrains.com/issue/KT-8575
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.toLowerCase()
private operator fun JTextComponent.getValue(a: AceSettingsPanel, p: KProperty<*>) = text.lowercase()
private operator fun JTextComponent.setValue(a: AceSettingsPanel, p: KProperty<*>, s: String) = setText(s)
private operator fun ColorPanel.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedColor

View File

@@ -5,13 +5,22 @@ package org.acejump.input
* ergonomically difficult they are to press.
*/
@Suppress("unused", "SpellCheckingInspection")
enum class KeyLayout(internal val rows: Array<String>, priority: String, internal val characterRemapping: Map<Char, Char> = emptyMap()) {
enum class KeyLayout(
internal val rows: Array<String>,
priority: String,
private val characterSides: Pair<Set<Char>, Set<Char>> = Pair(emptySet(), emptySet()),
internal val characterRemapping: Map<Char, Char> = emptyMap(),
) {
COLEMK(arrayOf("1234567890", "qwfpgjluy", "arstdhneio", "zxcvbkm"), priority = "tndhseriaovkcmbxzgjplfuwyq5849673210"),
WORKMN(arrayOf("1234567890", "qdrwbjfup", "ashtgyneoi", "zxmcvkl"), priority = "tnhegysoaiclvkmxzwfrubjdpq5849673210"),
DVORAK(arrayOf("1234567890", "pyfgcrl", "aoeuidhtns", "qjkxbmwvz"), priority = "uhetidonasxkbjmqwvzgfycprl5849673210"),
QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210"),
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
QWERTZ_CZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterRemapping = mapOf(
QWERTZ_CZ(
arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"),
priority = "fjghdkslavncmbxyrutzeiwoqp5849673210",
characterSides = sides("", ""),
characterRemapping = mapOf(
'+' to '1',
'ě' to '2',
'š' to '3',
@@ -22,15 +31,20 @@ enum class KeyLayout(internal val rows: Array<String>, priority: String, interna
'á' to '8',
'í' to '9',
'é' to '0'
)),
)
),
QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"),
QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"),
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210");
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
internal val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
private val allPriorities = priority.mapIndexed { index, char -> char to index }.toMap()
internal inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int {
return { allPriorities.getOrDefault(tagToChar(it), Int.MAX_VALUE) }
internal fun priority(): (Char) -> Int {
return { allPriorities.getOrDefault(it, Int.MAX_VALUE) }
}
}
private fun sides(left: String, right: String): Pair<Set<Char>, Set<Char>> {
return Pair(left.toCharArray().toSet(), right.toCharArray().toSet())
}

View File

@@ -7,17 +7,14 @@ import org.acejump.config.AceSettings
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/
internal object KeyLayoutCache {
/**
* Returns all possible two key tags, pre-sorted according to [tagOrder].
*/
lateinit var allPossibleTagsLowercase: List<String>
lateinit var allowedCharsSorted: List<Char>
private set
/**
* Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing.
*/
fun ensureInitialized(settings: AceSettings) {
if (!::allPossibleTagsLowercase.isInitialized) {
if (!::allowedCharsSorted.isInitialized) {
reset(settings)
}
}
@@ -26,22 +23,17 @@ internal object KeyLayoutCache {
* Re-initializes cached data according to updated settings.
*/
fun reset(settings: AceSettings) {
@Suppress("ConvertLambdaToReference")
val allSuffixChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars).toList() }
val allPrefixChars = processCharList(settings.prefixChars).filterNot(allSuffixChars::contains).plus("")
val allowedCharList = processCharList(settings.allowedChars)
val tagOrder = compareBy(
String::length,
{ if (it.length == 1) Int.MIN_VALUE else allPrefixChars.indexOf(it.first().toString()) },
settings.layout.priority(String::last)
)
allPossibleTagsLowercase = allSuffixChars
.flatMap { suffix -> allPrefixChars.map { prefix -> "$prefix$suffix" } }
.sortedWith(tagOrder)
allowedCharsSorted = if (allowedCharList.isEmpty()) {
processCharList(settings.layout.allChars)
}
else {
allowedCharList.sortedWith(compareBy(settings.layout.priority()))
}
}
private fun processCharList(charList: String): Set<String> {
return charList.toCharArray().map(Char::lowercase).toSet()
private fun processCharList(charList: String): List<Char> {
return charList.toCharArray().map(Char::lowercaseChar).distinct()
}
}

View File

@@ -28,13 +28,13 @@ internal sealed class SearchQuery {
* If the first character of the query is lowercase, then the entire query will be case-insensitive,
* and only beginnings of words and camel humps will be matched.
*/
class Literal(override val rawText: String, val excludeMiddlesOfWords: Boolean) : SearchQuery() {
class Literal(override val rawText: String) : SearchQuery() {
init {
require(rawText.isNotEmpty())
}
override fun refine(char: Char): SearchQuery {
return Literal(rawText + char, excludeMiddlesOfWords)
return Literal(rawText + char)
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
@@ -44,19 +44,14 @@ internal sealed class SearchQuery {
override fun toRegex(): Regex {
val firstChar = rawText.first()
val pattern = if (firstChar.isLowerCase()) {
if (excludeMiddlesOfWords) {
val firstCharUppercasePattern = Regex.escape(firstChar.uppercaseChar().toString())
val firstCharPattern = Regex.escape(firstChar.toString())
val remainingPattern = if (rawText.length > 1) Regex.escape(rawText.drop(1)) else ""
"(?:$firstCharUppercasePattern|(?<![a-zA-Z])$firstCharPattern)$remainingPattern"
}
else {
val fullPattern = Regex.escape(rawText)
"(?i)$fullPattern"
}
}
else {
Regex.escape(rawText)
val firstCharUppercasePattern = Regex.escape(firstChar.toString())
val firstCharLowercasePattern = Regex.escape(firstChar.lowercase())
val remainingPattern = if (rawText.length > 1) Regex.escape(rawText.drop(1)) else ""
"(?:$firstCharUppercasePattern|(?<![a-zA-Z])$firstCharLowercasePattern)$remainingPattern"
}
return Regex(pattern, setOf(RegexOption.MULTILINE))
@@ -70,7 +65,7 @@ internal sealed class SearchQuery {
override val rawText = ""
override fun refine(char: Char): SearchQuery {
return Literal(char.toString(), excludeMiddlesOfWords = false)
return Literal(char.toString())
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int {

View File

@@ -40,7 +40,7 @@ class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
.flatMap { (editor, sites) -> sites.map { site -> Tag(editor, site) } }
.sortedWith(siteOrder(editors, caches))
tagMap = KeyLayoutCache.allPossibleTagsLowercase.zip(tagSites).toMap()
tagMap = generateTags(tagSites).zip(tagSites).toMap()
}
internal fun type(char: Char): TaggingResult {
@@ -61,6 +61,35 @@ class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
}
private companion object {
private fun generateTags(tagSites: List<Tag>): List<String> {
val allowedChars = KeyLayoutCache.allowedCharsSorted
val tags = mutableListOf<String>()
var remainingTagCount = tagSites.size
outer@ for (i in allowedChars.indices) {
val c1 = allowedChars[i]
if (remainingTagCount <= allowedChars.size - i) {
tags.add(c1.toString())
if (--remainingTagCount <= 0) {
break@outer
}
}
else {
for (c2 in allowedChars) {
tags.add("$c1$c2")
if (--remainingTagCount <= 0) {
break@outer
}
}
}
}
return tags
}
private fun sortResults(results: Map<Editor, IntList>, caches: Map<Editor, EditorOffsetCache>) {
for ((editor, offsets) in results) {
val cache = caches.getValue(editor)

View File

@@ -2,6 +2,7 @@ package org.acejump.session
import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.search.SearchProcessor
import org.acejump.search.SearchQuery
@@ -17,7 +18,7 @@ sealed interface SessionState {
private val defaultBoundary: Boundaries,
) : SessionState {
override fun type(char: Char): TypeResult {
val searchProcessor = SearchProcessor(jumpEditors, SearchQuery.Literal(char.toString(), excludeMiddlesOfWords = true), defaultBoundary)
val searchProcessor = SearchProcessor(jumpEditors, SearchQuery.Literal(char.toString()), defaultBoundary)
return if (searchProcessor.isQueryFinished) {
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
@@ -63,9 +64,15 @@ sealed interface SessionState {
override fun type(char: Char): TypeResult {
if (char == ' ') {
val query = searchProcessor.query
if (query is SearchQuery.Literal && query.excludeMiddlesOfWords) {
val newQuery = SearchQuery.Literal(query.rawText, excludeMiddlesOfWords = false)
val newSearchProcessor = SearchProcessor(jumpEditors, newQuery, searchProcessor.boundaries)
if (query is SearchQuery.Literal) {
val newBoundaries = when (searchProcessor.boundaries) {
StandardBoundaries.VISIBLE_ON_SCREEN -> StandardBoundaries.AFTER_CARET
StandardBoundaries.AFTER_CARET -> StandardBoundaries.BEFORE_CARET
StandardBoundaries.BEFORE_CARET -> StandardBoundaries.VISIBLE_ON_SCREEN
else -> searchProcessor.boundaries
}
val newSearchProcessor = SearchProcessor(jumpEditors, query, newBoundaries)
return TypeResult.ChangeState(SelectTag(actions, jumpEditors, newSearchProcessor))
}
}