1
0
mirror of https://github.com/chylex/IntelliJ-AceJump.git synced 2025-09-16 07:24:50 +02:00

11 Commits

13 changed files with 166 additions and 117 deletions

View File

@@ -4,23 +4,26 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.9.10" kotlin("jvm") version "1.9.10"
id("org.jetbrains.intellij") version "1.16.1" id("org.jetbrains.intellij") version "1.17.3"
} }
group = "org.acejump" group = "org.acejump"
version = "chylex-18" version = "chylex-21"
repositories { repositories {
mavenCentral() mavenCentral()
} }
intellij { intellij {
version.set("2023.3") version.set("2024.2")
updateSinceUntilBuild.set(false) updateSinceUntilBuild.set(false)
plugins.add("IdeaVIM:chylex-22")
plugins.add("IdeaVIM:chylex-40")
plugins.add("com.intellij.classic.ui:242.20224.159")
pluginsRepositories { pluginsRepositories {
custom("https://intellij.chylex.com") custom("https://intellij.chylex.com")
marketplace()
} }
} }
@@ -33,7 +36,7 @@ dependencies {
} }
tasks.patchPluginXml { tasks.patchPluginXml {
sinceBuild.set("233") sinceBuild.set("242")
} }
tasks.buildSearchableOptions { tasks.buildSearchableOptions {

View File

@@ -1,5 +1,6 @@
package org.acejump.action package org.acejump.action
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
@@ -10,13 +11,12 @@ import com.maddyhome.idea.vim.action.change.change.ChangeVisualAction
import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction
import com.maddyhome.idea.vim.action.copy.YankVisualAction import com.maddyhome.idea.vim.action.copy.YankVisualAction
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.MappingMode.OP_PENDING
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.vimSetSelection import com.maddyhome.idea.vim.group.visual.vimSetSelection
import com.maddyhome.idea.vim.helper.inVisualMode import com.maddyhome.idea.vim.helper.inVisualMode
import com.maddyhome.idea.vim.helper.vimSelectionStart import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType import com.maddyhome.idea.vim.state.mode.SelectionType
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
@@ -52,12 +52,11 @@ sealed class AceVimAction : DumbAwareAction() {
} }
else { else {
val vim = editor.vim val vim = editor.vim
val commandState = vim.vimStateMachine val keyHandler = KeyHandler.getInstance()
if (commandState.isOperatorPending) { if (keyHandler.isOperatorPending(vim.mode, keyHandler.keyHandlerState)) {
val key = commandState.commandBuilder.keys.singleOrNull()?.keyChar val key = keyHandler.keyHandlerState.commandBuilder.keys.singleOrNull()?.keyChar
commandState.reset() keyHandler.fullReset(vim)
KeyHandler.getInstance().fullReset(vim)
AceVimUtil.enterVisualMode(vim, SelectionType.CHARACTER_WISE) AceVimUtil.enterVisualMode(vim, SelectionType.CHARACTER_WISE)
caret.vim.vimSetSelection(caret.offset, initialOffset, moveCaretToSelectionEnd = true) caret.vim.vimSetSelection(caret.offset, initialOffset, moveCaretToSelectionEnd = true)
@@ -72,15 +71,16 @@ sealed class AceVimAction : DumbAwareAction() {
if (action != null) { if (action != null) {
ApplicationManager.getApplication().invokeLater { ApplicationManager.getApplication().invokeLater {
WriteAction.run<Nothing> { WriteAction.run<Nothing> {
commandState.commandBuilder.pushCommandPart(action) keyHandler.keyHandlerState.commandBuilder.pushCommandPart(action)
val cmd = commandState.commandBuilder.buildCommand() val cmd = keyHandler.keyHandlerState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(commandState.mappingState.mappingMode == OP_PENDING, cmd.rawCount, commandState.mode) val operatorArguments = OperatorArguments(vim.mode is Mode.OP_PENDING, cmd.rawCount, injector.vimState.mode)
commandState.executingCommand = cmd injector.vimState.executingCommand = cmd
injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments) injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments)
// TODO does not update status
} }
keyHandler.reset(vim)
} }
} }
} }
@@ -168,6 +168,10 @@ sealed class AceVimAction : DumbAwareAction() {
action.presentation.isEnabled = action.getData(CommonDataKeys.EDITOR) != null action.presentation.isEnabled = action.getData(CommonDataKeys.EDITOR) != null
} }
override fun getActionUpdateThread(): ActionUpdateThread {
return ActionUpdateThread.BGT
}
override fun actionPerformed(e: AnActionEvent) { override fun actionPerformed(e: AnActionEvent) {
val editor = e.getData(CommonDataKeys.EDITOR) ?: return val editor = e.getData(CommonDataKeys.EDITOR) ?: return
val session = SessionManager.start(editor, AceVimMode.JumpAllEditors.getJumpEditors(editor)) val session = SessionManager.start(editor, AceVimMode.JumpAllEditors.getJumpEditors(editor))

View File

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

View File

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

View File

@@ -5,32 +5,46 @@ package org.acejump.input
* ergonomically difficult they are to press. * ergonomically difficult they are to press.
*/ */
@Suppress("unused", "SpellCheckingInspection") @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"), COLEMK(arrayOf("1234567890", "qwfpgjluy", "arstdhneio", "zxcvbkm"), priority = "tndhseriaovkcmbxzgjplfuwyq5849673210"),
WORKMN(arrayOf("1234567890", "qdrwbjfup", "ashtgyneoi", "zxmcvkl"), priority = "tnhegysoaiclvkmxzwfrubjdpq5849673210"), WORKMN(arrayOf("1234567890", "qdrwbjfup", "ashtgyneoi", "zxmcvkl"), priority = "tnhegysoaiclvkmxzwfrubjdpq5849673210"),
DVORAK(arrayOf("1234567890", "pyfgcrl", "aoeuidhtns", "qjkxbmwvz"), priority = "uhetidonasxkbjmqwvzgfycprl5849673210"), DVORAK(arrayOf("1234567890", "pyfgcrl", "aoeuidhtns", "qjkxbmwvz"), priority = "uhetidonasxkbjmqwvzgfycprl5849673210"),
QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210"), QWERTY(arrayOf("1234567890", "qwertyuiop", "asdfghjkl", "zxcvbnm"), priority = "fjghdkslavncmbxzrutyeiwoqp5849673210"),
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"), QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
QWERTZ_CZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterRemapping = mapOf( QWERTZ_CZ(
'+' to '1', arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"),
'ě' to '2', priority = "fjghdkslavncmbxyrutzeiwoqp5849673210",
'š' to '3', characterSides = sides("", ""),
'č' to '4', characterRemapping = mapOf(
'ř' to '5', '+' to '1',
'ž' to '6', 'ě' to '2',
'ý' to '7', 'š' to '3',
'á' to '8', 'č' to '4',
'í' to '9', 'ř' to '5',
'é' to '0' 'ž' to '6',
)), 'ý' to '7',
'á' to '8',
'í' to '9',
'é' to '0'
)
),
QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"), QGMLWY(arrayOf("1234567890", "qgmlwyfub", "dstnriaeoh", "zxcvjkp"), priority = "naterisodhvkcpjxzlfmuwygbq5849673210"),
QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"), QGMLWB(arrayOf("1234567890", "qgmlwbyuv", "dstnriaeoh", "zxcfjkp"), priority = "naterisodhfkcpjxzlymuwbgvq5849673210"),
NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210"); NORMAN(arrayOf("1234567890", "qwdfkjurl", "asetgynioh", "zxcvbpm"), priority = "tneigysoahbvpcmxzjkufrdlwq5849673210");
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("") 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 { internal fun priority(): (Char) -> Int {
return { allPriorities.getOrDefault(tagToChar(it), Int.MAX_VALUE) } 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). * with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/ */
internal object KeyLayoutCache { internal object KeyLayoutCache {
/** lateinit var allowedCharsSorted: List<Char>
* Returns all possible two key tags, pre-sorted according to [tagOrder].
*/
lateinit var allPossibleTagsLowercase: List<String>
private set private set
/** /**
* Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing. * Called before any lazily initialized properties are used, to ensure that they are initialized even if the settings are missing.
*/ */
fun ensureInitialized(settings: AceSettings) { fun ensureInitialized(settings: AceSettings) {
if (!::allPossibleTagsLowercase.isInitialized) { if (!::allowedCharsSorted.isInitialized) {
reset(settings) reset(settings)
} }
} }
@@ -26,22 +23,17 @@ internal object KeyLayoutCache {
* Re-initializes cached data according to updated settings. * Re-initializes cached data according to updated settings.
*/ */
fun reset(settings: AceSettings) { fun reset(settings: AceSettings) {
@Suppress("ConvertLambdaToReference") val allowedCharList = processCharList(settings.allowedChars)
val allSuffixChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars).toList() }
val allPrefixChars = processCharList(settings.prefixChars).filterNot(allSuffixChars::contains).plus("")
val tagOrder = compareBy( allowedCharsSorted = if (allowedCharList.isEmpty()) {
String::length, processCharList(settings.layout.allChars)
{ if (it.length == 1) Int.MIN_VALUE else allPrefixChars.indexOf(it.first().toString()) }, }
settings.layout.priority(String::last) else {
) allowedCharList.sortedWith(compareBy(settings.layout.priority()))
}
allPossibleTagsLowercase = allSuffixChars
.flatMap { suffix -> allPrefixChars.map { prefix -> "$prefix$suffix" } }
.sortedWith(tagOrder)
} }
private fun processCharList(charList: String): Set<String> { private fun processCharList(charList: String): List<Char> {
return charList.toCharArray().map(Char::lowercase).toSet() return charList.toCharArray().map(Char::lowercaseChar).distinct()
} }
} }

View File

@@ -11,18 +11,8 @@ import org.acejump.matchesAt
/** /**
* Searches editor text for matches of a [SearchQuery], and updates previous results when the user [refineQuery]s a character. * Searches editor text for matches of a [SearchQuery], and updates previous results when the user [refineQuery]s a character.
*/ */
class SearchProcessor private constructor(query: SearchQuery, private val results: MutableMap<Editor, IntArrayList>) { class SearchProcessor private constructor(query: SearchQuery, val boundaries: Boundaries, private val results: MutableMap<Editor, IntArrayList>) {
companion object { internal constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, boundaries, mutableMapOf()) {
fun fromString(editors: List<Editor>, query: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.Literal(query), boundaries)
}
fun fromRegex(editors: List<Editor>, pattern: String, boundaries: Boundaries): SearchProcessor {
return SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
}
}
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
val regex = query.toRegex() val regex = query.toRegex()
if (regex != null) { if (regex != null) {
@@ -65,7 +55,7 @@ class SearchProcessor private constructor(query: SearchQuery, private val result
return true return true
} }
else { else {
query = SearchQuery.Literal(query.rawText + char) query = query.refine(char)
removeObsoleteResults() removeObsoleteResults()
return isQueryFinished return isQueryFinished
} }

View File

@@ -8,6 +8,11 @@ import org.acejump.countMatchingCharacters
internal sealed class SearchQuery { internal sealed class SearchQuery {
abstract val rawText: String abstract val rawText: String
/**
* Returns a new query with the given character appended.
*/
abstract fun refine(char: Char): SearchQuery
/** /**
* Returns how many characters the search occurrence highlight should cover. * Returns how many characters the search occurrence highlight should cover.
*/ */
@@ -19,29 +24,37 @@ internal sealed class SearchQuery {
abstract fun toRegex(): Regex? abstract fun toRegex(): Regex?
/** /**
* Searches for all occurrences of a literal text query. If the first character of the query is lowercase, then the entire query will be * Searches for all occurrences of a literal text query.
* case-insensitive. * 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.
* Each occurrence must either match the entire query, or match the query up to a point so that the rest of the query matches the
* beginning of a tag at the location of the occurrence.
*/ */
class Literal(override val rawText: String) : SearchQuery() { class Literal(override val rawText: String) : SearchQuery() {
init { init {
require(rawText.isNotEmpty()) require(rawText.isNotEmpty())
} }
override fun refine(char: Char): SearchQuery {
return Literal(rawText + char)
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int { override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return text.countMatchingCharacters(offset, rawText) return text.countMatchingCharacters(offset, rawText)
} }
override fun toRegex(): Regex { override fun toRegex(): Regex {
val options = mutableSetOf(RegexOption.MULTILINE) val firstChar = rawText.first()
val pattern = if (firstChar.isLowerCase()) {
if (rawText.first().isLowerCase()) { val fullPattern = Regex.escape(rawText)
options.add(RegexOption.IGNORE_CASE) "(?i)$fullPattern"
}
else {
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(Regex.escape(rawText), options) return Regex(pattern, setOf(RegexOption.MULTILINE))
} }
} }
@@ -51,6 +64,10 @@ internal sealed class SearchQuery {
class RegularExpression(private val pattern: String) : SearchQuery() { class RegularExpression(private val pattern: String) : SearchQuery() {
override val rawText = "" override val rawText = ""
override fun refine(char: Char): SearchQuery {
return Literal(char.toString())
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int { override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return 1 return 1
} }

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) } } .flatMap { (editor, sites) -> sites.map { site -> Tag(editor, site) } }
.sortedWith(siteOrder(editors, caches)) .sortedWith(siteOrder(editors, caches))
tagMap = KeyLayoutCache.allPossibleTagsLowercase.zip(tagSites).toMap() tagMap = generateTags(tagSites).zip(tagSites).toMap()
} }
internal fun type(char: Char): TaggingResult { 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 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>) { private fun sortResults(results: Map<Editor, IntList>, caches: Map<Editor, EditorOffsetCache>) {
for ((editor, offsets) in results) { for ((editor, offsets) in results) {
val cache = caches.getValue(editor) val cache = caches.getValue(editor)

View File

@@ -75,7 +75,7 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
val state = state ?: return val state = state ?: return
editorSettings.startEditing(editor) editorSettings.startEditing(editor)
val result = mode.type(state, AceConfig.layout.characterRemapping.getOrDefault(charTyped, charTyped), acceptedTag) val result = mode.type(state, charTyped, acceptedTag)
editorSettings.stopEditing(editor) editorSettings.stopEditing(editor)
when (result) { when (result) {
@@ -128,7 +128,7 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
canvas.setMarkers(emptyList()) canvas.setMarkers(emptyList())
} }
val processor = SearchProcessor.fromRegex(jumpEditors, pattern.regex, defaultBoundary) val processor = SearchProcessor(jumpEditors, SearchQuery.RegularExpression(pattern.regex), defaultBoundary)
textHighlighter.renderOccurrences(processor.resultsCopy, processor.query) textHighlighter.renderOccurrences(processor.resultsCopy, processor.query)
state = SessionState.SelectTag(actions, jumpEditors, processor) state = SessionState.SelectTag(actions, jumpEditors, processor)

View File

@@ -2,7 +2,10 @@ package org.acejump.session
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.search.SearchProcessor import org.acejump.search.SearchProcessor
import org.acejump.search.SearchQuery
import org.acejump.search.Tagger import org.acejump.search.Tagger
import org.acejump.search.TaggingResult import org.acejump.search.TaggingResult
@@ -15,7 +18,7 @@ sealed interface SessionState {
private val defaultBoundary: Boundaries, private val defaultBoundary: Boundaries,
) : SessionState { ) : SessionState {
override fun type(char: Char): TypeResult { override fun type(char: Char): TypeResult {
val searchProcessor = SearchProcessor.fromString(jumpEditors, char.toString(), defaultBoundary) val searchProcessor = SearchProcessor(jumpEditors, SearchQuery.Literal(char.toString()), defaultBoundary)
return if (searchProcessor.isQueryFinished) { return if (searchProcessor.isQueryFinished) {
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor)) TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
@@ -49,8 +52,8 @@ sealed interface SessionState {
class SelectTag internal constructor( class SelectTag internal constructor(
private val actions: SessionActions, private val actions: SessionActions,
jumpEditors: List<Editor>, private val jumpEditors: List<Editor>,
searchProcessor: SearchProcessor, private val searchProcessor: SearchProcessor,
) : SessionState { ) : SessionState {
private val tagger = Tagger(jumpEditors, searchProcessor.resultsCopy) private val tagger = Tagger(jumpEditors, searchProcessor.resultsCopy)
@@ -59,7 +62,22 @@ sealed interface SessionState {
} }
override fun type(char: Char): TypeResult { override fun type(char: Char): TypeResult {
return when (val result = tagger.type(char)) { if (char == ' ') {
val query = searchProcessor.query
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))
}
}
return when (val result = tagger.type(AceConfig.layout.characterRemapping.getOrDefault(char, char))) {
is TaggingResult.Nothing -> TypeResult.Nothing is TaggingResult.Nothing -> TypeResult.Nothing
is TaggingResult.Accept -> TypeResult.AcceptTag(result.tag) is TaggingResult.Accept -> TypeResult.AcceptTag(result.tag)
is TaggingResult.Mark -> { is TaggingResult.Mark -> {