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

21 Commits

Author SHA1 Message Date
82de69d53c WIP 2024-09-03 22:16:35 +02:00
ffb4573eda Redo tag generation (eliminate explicit prefix chars) 2024-09-03 21:50:03 +02:00
01c38df82a Search in the middle of words again unless pressing an uppercase letter, and rebind Space to cycling search boundaries 2024-09-03 21:50:03 +02:00
a3a86cf447 Update for IDEA 2024.2 2024-09-03 20:58:08 +02:00
59fbd4e19c Set version to chylex-21 2024-07-15 13:46:02 +02:00
6e08d56cdf Update for IDEA 2024.1 & latest IdeaVim 2024-07-15 13:45:39 +02:00
9a14fb87e3 Set version to chylex-20 2024-03-27 13:53:41 +01:00
d22bcc220e Update IdeaVim integration 2024-03-27 13:53:25 +01:00
1f76a8bd25 Set version to chylex-19 2023-12-29 21:45:12 +01:00
2e31ddd504 Use top row remapping only for tags 2023-12-29 21:45:12 +01:00
47a34f6f14 Disable searching in the middle of a word unless Space is pressed when typing tag 2023-12-29 21:45:12 +01:00
575283b2be Set version to chylex-18 2023-12-18 05:25:09 +01:00
8bb34d7f75 Make editor fade opacity configurable 2023-12-18 05:24:57 +01:00
8d1ef031d2 Set version to chylex-17 2023-12-18 03:44:20 +01:00
a9375ec414 Remove unused code 2023-12-18 03:44:01 +01:00
b42112cc9e Do not try to tag folded regions 2023-12-18 02:36:41 +01:00
8e42c82821 Use distance from current caret for tag order 2023-12-18 02:32:54 +01:00
48bcf9f284 Add QWERTZ (CZ) layout that remaps top row to digits 2023-12-18 00:58:37 +01:00
2681d9901f Change priority order of two-character tags 2023-12-18 00:45:28 +01:00
b13d629046 Add option to set different colors for two-character tags 2023-12-17 22:41:36 +01:00
92dcd033fb Make priority of unknown characters lower than known characters 2023-12-17 21:49:24 +01:00
18 changed files with 263 additions and 206 deletions

View File

@@ -4,23 +4,26 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
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"
version = "chylex-16"
version = "chylex-21"
repositories {
mavenCentral()
}
intellij {
version.set("2023.3")
version.set("2024.2")
updateSinceUntilBuild.set(false)
plugins.add("IdeaVIM:chylex-22")
plugins.add("IdeaVIM:chylex-40")
plugins.add("com.intellij.classic.ui:242.20224.159")
pluginsRepositories {
custom("https://intellij.chylex.com")
marketplace()
}
}
@@ -33,7 +36,7 @@ dependencies {
}
tasks.patchPluginXml {
sinceBuild.set("233")
sinceBuild.set("242")
}
tasks.buildSearchableOptions {

View File

@@ -7,8 +7,6 @@ import com.intellij.openapi.project.Project
import com.intellij.util.IncorrectOperationException
import it.unimi.dsi.fastutil.ints.IntArrayList
annotation class ExternalUsage
/**
* Returns an immutable version of the currently edited document.
*/
@@ -52,57 +50,6 @@ fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): In
return i
}
/**
* Determines which characters form a "word" for the purposes of functions below.
*/
val Char.isWordPart
get() = this in 'a'..'z' || this.isJavaIdentifierPart()
/**
* Finds index of the first character in a word.
*/
inline fun CharSequence.wordStart(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var start = pos
while (start > 0 && isPartOfWord(this[start - 1])) {
--start
}
return start
}
/**
* Finds index of the last character in a word.
*/
inline fun CharSequence.wordEnd(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = pos
val limit = length - 1
while (end < limit && isPartOfWord(this[end + 1])) {
++end
}
return end
}
/**
* Finds index of the first word character following a sequence of non-word characters following the end of a word.
*/
inline fun CharSequence.wordEndPlus(pos: Int, isPartOfWord: (Char) -> Boolean = Char::isWordPart): Int {
var end = this.wordEnd(pos, isPartOfWord)
val limit = length - 1
while (end < limit && !isPartOfWord(this[end + 1])) {
++end
}
if (end < limit && isPartOfWord(this[end + 1])) {
++end
}
return end
}
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
val clone = HashMap<Editor, IntArrayList>(size)

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
@@ -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.copy.YankVisualAction
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.group.visual.vimSetSelection
import com.maddyhome.idea.vim.helper.inVisualMode
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.state.mode.Mode
import com.maddyhome.idea.vim.state.mode.SelectionType
import org.acejump.boundaries.StandardBoundaries.AFTER_CARET
import org.acejump.boundaries.StandardBoundaries.BEFORE_CARET
@@ -52,12 +52,11 @@ sealed class AceVimAction : DumbAwareAction() {
}
else {
val vim = editor.vim
val commandState = vim.vimStateMachine
if (commandState.isOperatorPending) {
val key = commandState.commandBuilder.keys.singleOrNull()?.keyChar
val keyHandler = KeyHandler.getInstance()
if (keyHandler.isOperatorPending(vim.mode, keyHandler.keyHandlerState)) {
val key = keyHandler.keyHandlerState.commandBuilder.keys.singleOrNull()?.keyChar
commandState.reset()
KeyHandler.getInstance().fullReset(vim)
keyHandler.fullReset(vim)
AceVimUtil.enterVisualMode(vim, SelectionType.CHARACTER_WISE)
caret.vim.vimSetSelection(caret.offset, initialOffset, moveCaretToSelectionEnd = true)
@@ -72,15 +71,16 @@ sealed class AceVimAction : DumbAwareAction() {
if (action != null) {
ApplicationManager.getApplication().invokeLater {
WriteAction.run<Nothing> {
commandState.commandBuilder.pushCommandPart(action)
keyHandler.keyHandlerState.commandBuilder.pushCommandPart(action)
val cmd = commandState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(commandState.mappingState.mappingMode == OP_PENDING, cmd.rawCount, commandState.mode)
val cmd = keyHandler.keyHandlerState.commandBuilder.buildCommand()
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)
// TODO does not update status
}
keyHandler.reset(vim)
}
}
}
@@ -168,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

@@ -20,8 +20,10 @@ class AceConfig : PersistentStateComponent<AceSettings> {
val layout get() = settings.layout
val minQueryLength get() = settings.minQueryLength
val editorFadeOpacity get() = settings.editorFadeOpacity
val jumpModeColor get() = settings.jumpModeColor
val tagForegroundColor get() = settings.tagForegroundColor
val tagForegroundColor1 get() = settings.tagForegroundColor1
val tagForegroundColor2 get() = settings.tagForegroundColor2
val searchHighlightColor get() = settings.searchHighlightColor
}

View File

@@ -13,20 +13,22 @@ 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 ||
panel.jumpModeColor != settings.jumpModeColor ||
panel.tagForegroundColor != settings.tagForegroundColor ||
panel.tagForegroundColor1 != settings.tagForegroundColor1 ||
panel.tagForegroundColor2 != settings.tagForegroundColor2 ||
panel.searchHighlightColor != settings.searchHighlightColor
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
panel.jumpModeColor?.let { settings.jumpModeColor = it }
panel.tagForegroundColor?.let { settings.tagForegroundColor = it }
panel.tagForegroundColor1?.let { settings.tagForegroundColor1 = it }
panel.tagForegroundColor2?.let { settings.tagForegroundColor2 = it }
panel.searchHighlightColor?.let { settings.searchHighlightColor = it }
KeyLayoutCache.reset(settings)
}

View File

@@ -8,14 +8,17 @@ 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,
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
var jumpModeColor: Color = Color(0xFFFFFF),
@OptionTag("tagForegroundRGB", converter = ColorConverter::class)
var tagForegroundColor: Color = Color(0xFFFFFF),
var tagForegroundColor1: Color = Color(0xFFFFFF),
@OptionTag("tagForeground2RGB", converter = ColorConverter::class)
var tagForegroundColor2: Color = Color(0xFFFFFF),
@OptionTag("searchHighlightRGB", converter = ColorConverter::class)
var searchHighlightColor: Color = Color(0x008299),

View File

@@ -2,18 +2,21 @@ package org.acejump.config
import com.intellij.openapi.ui.ComboBox
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
import javax.swing.text.JTextComponent
import kotlin.reflect.KProperty
@@ -23,70 +26,89 @@ 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()
private val editorFadeOpacitySlider = JBSlider(0, 10).apply {
labelTable = Hashtable((0..10).associateWith { JLabel("${it * 10}") })
paintTrack = true
paintLabels = true
paintTicks = true
minorTickSpacing = 1
majorTickSpacing = 1
}
private val jumpModeColorWheel = ColorPanel()
private val tagForegroundColorWheel = ColorPanel()
private val tagForeground1ColorWheel = ColorPanel()
private val tagForeground2ColorWheel = ColorPanel()
private val searchHighlightColorWheel = ColorPanel()
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") {
row("Caret background:") { short(jumpModeColorWheel) }
row("Tag foreground:") { short(tagForegroundColorWheel) }
row("Search highlight:") { short(searchHighlightColorWheel) }
group("Colors") {
row("Caret background:") {
cell(jumpModeColorWheel)
}
row("Tag foreground:") {
cell(tagForeground1ColorWheel)
cell(tagForeground2ColorWheel)
}
row("Search highlight:") {
cell(searchHighlightColorWheel)
}
row("Editor fade opacity (%):") {
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
internal var editorFadeOpacity by editorFadeOpacitySlider
internal var jumpModeColor by jumpModeColorWheel
internal var tagForegroundColor by tagForegroundColorWheel
internal var tagForegroundColor1 by tagForeground1ColorWheel
internal var tagForegroundColor2 by tagForeground2ColorWheel
internal var searchHighlightColor by searchHighlightColorWheel
internal var minQueryLengthInt
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
set(value) { minQueryLength = value.toString() }
internal var editorFadeOpacityPercent
get() = editorFadeOpacity * 10
set(value) { editorFadeOpacity = value / 10 }
fun reset(settings: AceSettings) {
allowedChars = settings.allowedChars
prefixChars = settings.prefixChars
keyboardLayout = settings.layout
minQueryLength = settings.minQueryLength.toString()
editorFadeOpacityPercent = settings.editorFadeOpacity
jumpModeColor = settings.jumpModeColor
tagForegroundColor = settings.tagForegroundColor
tagForegroundColor1 = settings.tagForegroundColor1
tagForegroundColor2 = settings.tagForegroundColor2
searchHighlightColor = settings.searchHighlightColor
}
// 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
@@ -95,6 +117,9 @@ internal class AceSettingsPanel {
private operator fun JCheckBox.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected
private operator fun JCheckBox.setValue(a: AceSettingsPanel, p: KProperty<*>, selected: Boolean) = setSelected(selected)
private operator fun JSlider.getValue(a: AceSettingsPanel, p: KProperty<*>) = value
private operator fun JSlider.setValue(a: AceSettingsPanel, p: KProperty<*>, value: Int) = setValue(value)
private operator fun <T> ComboBox<T>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item)

View File

@@ -4,21 +4,47 @@ package org.acejump.input
* Defines common keyboard layouts. Each layout has a key priority order, based on each key's distance from the home row and how
* ergonomically difficult they are to press.
*/
@Suppress("unused")
enum class KeyLayout(internal val rows: Array<String>, priority: String) {
@Suppress("unused", "SpellCheckingInspection")
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",
characterSides = sides("", ""),
characterRemapping = mapOf(
'+' to '1',
'ě' to '2',
'š' to '3',
'č' to '4',
'ř' to '5',
'ž' to '6',
'ý' to '7',
'á' 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[tagToChar(it)] }
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,24 +7,14 @@ import org.acejump.config.AceSettings
* with repeated keys (ex. FF, JJ) or adjacent keys (ex. GH, UJ).
*/
internal object KeyLayoutCache {
/**
* Sorts tags according to current keyboard layout settings, and some predefined rules that force tags with digits, and tags with two
* keys far apart, to be sorted after other (easier to type) tags.
*/
lateinit var tagOrder: Comparator<String>
private set
/**
* 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 (!::tagOrder.isInitialized) {
if (!::allowedCharsSorted.isInitialized) {
reset(settings)
}
}
@@ -33,21 +23,17 @@ internal object KeyLayoutCache {
* Re-initializes cached data according to updated settings.
*/
fun reset(settings: AceSettings) {
tagOrder = compareBy(
String::length,
settings.layout.priority(String::last)
)
val allowedCharList = processCharList(settings.allowedChars)
@Suppress("ConvertLambdaToReference")
val allSuffixChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars).toList() }
val allPrefixChars = processCharList(settings.prefixChars).filterNot(allSuffixChars::contains).plus("")
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

@@ -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.
*/
class SearchProcessor private constructor(query: SearchQuery, private val results: MutableMap<Editor, IntArrayList>) {
companion object {
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()) {
class SearchProcessor private constructor(query: SearchQuery, val boundaries: Boundaries, private val results: MutableMap<Editor, IntArrayList>) {
internal constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, boundaries, mutableMapOf()) {
val regex = query.toRegex()
if (regex != null) {
@@ -39,7 +29,7 @@ class SearchProcessor private constructor(query: SearchQuery, private val result
if (highlightEnd > offsetRange.last) {
break
}
else if (boundaries.isOffsetInside(editor, index)) {
else if (boundaries.isOffsetInside(editor, index) && !editor.foldingModel.isOffsetCollapsed(index)) {
offsets.add(index)
}
@@ -65,7 +55,7 @@ class SearchProcessor private constructor(query: SearchQuery, private val result
return true
}
else {
query = SearchQuery.Literal(query.rawText + char)
query = query.refine(char)
removeObsoleteResults()
return isQueryFinished
}

View File

@@ -8,6 +8,11 @@ import org.acejump.countMatchingCharacters
internal sealed class SearchQuery {
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.
*/
@@ -19,29 +24,37 @@ internal sealed class SearchQuery {
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
* case-insensitive.
*
* 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.
* Searches for all occurrences of a literal text query.
* 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) : SearchQuery() {
init {
require(rawText.isNotEmpty())
}
override fun refine(char: Char): SearchQuery {
return Literal(rawText + char)
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return text.countMatchingCharacters(offset, rawText)
}
override fun toRegex(): Regex {
val options = mutableSetOf(RegexOption.MULTILINE)
if (rawText.first().isLowerCase()) {
options.add(RegexOption.IGNORE_CASE)
val firstChar = rawText.first()
val pattern = if (firstChar.isLowerCase()) {
val fullPattern = Regex.escape(rawText)
"(?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() {
override val rawText = ""
override fun refine(char: Char): SearchQuery {
return Literal(char.toString())
}
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
return 1
}

View File

@@ -5,13 +5,10 @@ import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntList
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
import org.acejump.immutableText
import org.acejump.input.KeyLayoutCache
import org.acejump.isWordPart
import org.acejump.view.TagMarker
import kotlin.collections.component1
import kotlin.collections.component2
import kotlin.math.max
import kotlin.math.min
/**
@@ -43,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 {
@@ -64,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)
@@ -92,26 +118,24 @@ class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
}
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, caches.getValue(aEditor))
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, caches.getValue(bEditor))
val aCaches = caches.getValue(aEditor)
val bCaches = caches.getValue(bEditor)
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, aCaches)
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, bCaches)
if (aIsVisible != bIsVisible) {
// Sites in immediate view should come first.
return@Comparator if (aIsVisible) -1 else 1
}
val aIsNotWordStart = aEditor.immutableText[max(0, a.offset - 1)].isWordPart
val bIsNotWordStart = bEditor.immutableText[max(0, b.offset - 1)].isWordPart
if (aIsNotWordStart != bIsNotWordStart) {
// Ensure that the first letter of a word is prioritized for tagging.
return@Comparator if (bIsNotWordStart) -1 else 1
}
val aPosition = aCaches.offsetToXY(aEditor, a.offset)
val bPosition = bCaches.offsetToXY(bEditor, b.offset)
when {
a.offset < b.offset -> -1
a.offset > b.offset -> 1
else -> 0
}
val caretPosition = editorPriority[0].offsetToXY(editorPriority[0].caretModel.offset)
val aDistance = aPosition.distanceSq(caretPosition)
val bDistance = bPosition.distanceSq(caretPosition)
return@Comparator aDistance.compareTo(bDistance)
}
}
}

View File

@@ -128,7 +128,7 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
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)
state = SessionState.SelectTag(actions, jumpEditors, processor)

View File

@@ -2,7 +2,10 @@ 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
import org.acejump.search.Tagger
import org.acejump.search.TaggingResult
@@ -15,7 +18,7 @@ sealed interface SessionState {
private val defaultBoundary: Boundaries,
) : SessionState {
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) {
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
@@ -49,8 +52,8 @@ sealed interface SessionState {
class SelectTag internal constructor(
private val actions: SessionActions,
jumpEditors: List<Editor>,
searchProcessor: SearchProcessor,
private val jumpEditors: List<Editor>,
private val searchProcessor: SearchProcessor,
) : SessionState {
private val tagger = Tagger(jumpEditors, searchProcessor.resultsCopy)
@@ -59,7 +62,22 @@ sealed interface SessionState {
}
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.Accept -> TypeResult.AcceptTag(result.tag)
is TaggingResult.Mark -> {

View File

@@ -5,6 +5,7 @@ import com.intellij.openapi.editor.Editor
import com.intellij.ui.ColorUtil
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import java.awt.Graphics
import java.awt.Graphics2D
import java.awt.Rectangle
@@ -53,7 +54,7 @@ internal class TagCanvas(private val editor: Editor) : JComponent() {
return
}
g.color = ColorUtil.withAlpha(editor.colorsScheme.defaultBackground, 0.6)
g.color = ColorUtil.withAlpha(editor.colorsScheme.defaultBackground, (AceConfig.editorFadeOpacity * 0.01).coerceIn(0.0, 1.0))
g.fillRect(0, 0, width - 1, height - 1)
if (markers.isEmpty()) {

View File

@@ -12,7 +12,8 @@ internal class TagFont(editor: Editor) {
val tagFont: Font = editor.colorsScheme.getFont(EditorFontType.PLAIN)
val tagCharWidth = editor.component.getFontMetrics(tagFont).charWidth('W')
val foregroundColor = AceConfig.tagForegroundColor
val foregroundColor1 = AceConfig.tagForegroundColor1
val foregroundColor2 = AceConfig.tagForegroundColor2
val lineHeight = editor.lineHeight
val baselineDistance = editor.ascent

View File

@@ -12,10 +12,10 @@ import java.awt.Rectangle
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
*/
internal class TagMarker(
private val tag: String,
private val tag: CharArray,
val offset: Int
) {
private val length = tag.length
private val length = tag.size
companion object {
/**
@@ -28,26 +28,7 @@ internal class TagMarker(
* character ([typedTag]) matches the first [tag] character, only the second [tag] character is displayed.
*/
fun create(tag: String, offset: Int, typedTag: String): TagMarker {
return TagMarker(tag.drop(typedTag.length), offset)
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
g.translate(0.0, HIGHLIGHT_OFFSET)
g.fillRect(rect.x, rect.y, rect.width, rect.height + 1)
g.translate(0.0, -HIGHLIGHT_OFFSET)
}
/**
* Renders the tag text.
*/
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point, text: String) {
g.color = font.foregroundColor
g.font = font.tagFont
g.drawString(text, point.x, point.y + font.baselineDistance)
return TagMarker(tag.drop(typedTag.length).toCharArray(), offset)
}
}
@@ -59,12 +40,39 @@ internal class TagMarker(
val rect = alignTag(editor, cache, font, occupied) ?: return null
drawHighlight(g, rect, editor.colorsScheme.defaultBackground)
drawForeground(g, font, rect.location, tag)
drawForeground(g, font, rect.location)
occupied.add(rect)
return rect
}
/**
* Renders the tag background.
*/
private fun drawHighlight(g: Graphics2D, rect: Rectangle, color: Color) {
g.color = color
g.translate(0.0, HIGHLIGHT_OFFSET)
g.fillRect(rect.x, rect.y, rect.width, rect.height + 1)
g.translate(0.0, -HIGHLIGHT_OFFSET)
}
/**
* Renders the tag text.
*/
private fun drawForeground(g: Graphics2D, font: TagFont, point: Point) {
val x = point.x
val y = point.y + font.baselineDistance
g.font = font.tagFont
g.color = font.foregroundColor1
g.drawChars(tag, 0, 1, x, y)
if (tag.size > 1) {
g.color = font.foregroundColor2
g.drawChars(tag, 1, length - 1, x + font.tagCharWidth, y)
}
}
private fun alignTag(editor: Editor, cache: EditorOffsetCache, font: TagFont, occupied: List<Rectangle>): Rectangle? {
val pos = cache.offsetToXY(editor, offset)
val rect = Rectangle(pos.x, pos.y, font.tagCharWidth * length, font.lineHeight)