mirror of
https://github.com/chylex/IntelliJ-AceJump.git
synced 2025-09-15 14:32:08 +02:00
Compare commits
18 Commits
fa3505b850
...
broken-ide
Author | SHA1 | Date | |
---|---|---|---|
20ab421389
|
|||
59fbd4e19c
|
|||
6e08d56cdf
|
|||
9a14fb87e3
|
|||
d22bcc220e
|
|||
1f76a8bd25
|
|||
2e31ddd504
|
|||
47a34f6f14
|
|||
575283b2be
|
|||
8bb34d7f75
|
|||
8d1ef031d2
|
|||
a9375ec414
|
|||
b42112cc9e
|
|||
8e42c82821
|
|||
48bcf9f284
|
|||
2681d9901f
|
|||
b13d629046
|
|||
92dcd033fb
|
@@ -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")
|
||||
// custom("https://intellij.chylex.com")
|
||||
// marketplace()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +36,7 @@ dependencies {
|
||||
}
|
||||
|
||||
tasks.patchPluginXml {
|
||||
sinceBuild.set("233")
|
||||
sinceBuild.set("242")
|
||||
}
|
||||
|
||||
tasks.buildSearchableOptions {
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -10,13 +10,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 +51,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 +70,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -16,8 +16,10 @@ class AceConfigurable : Configurable {
|
||||
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() {
|
||||
@@ -25,8 +27,10 @@ class AceConfigurable : Configurable {
|
||||
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)
|
||||
}
|
||||
|
@@ -10,12 +10,16 @@ data class AceSettings(
|
||||
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),
|
||||
|
@@ -2,6 +2,7 @@ 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
|
||||
@@ -11,9 +12,12 @@ import com.intellij.ui.layout.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
|
||||
|
||||
@@ -27,8 +31,17 @@ internal class AceSettingsPanel {
|
||||
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 {
|
||||
@@ -54,9 +67,25 @@ internal class AceSettingsPanel {
|
||||
}
|
||||
|
||||
titledRow("Colors") {
|
||||
row("Caret background:") { short(jumpModeColorWheel) }
|
||||
row("Tag foreground:") { short(tagForegroundColorWheel) }
|
||||
row("Search highlight:") { short(searchHighlightColorWheel) }
|
||||
row("Caret background:") {
|
||||
cell {
|
||||
component(jumpModeColorWheel)
|
||||
}
|
||||
}
|
||||
row("Tag foreground:") {
|
||||
cell {
|
||||
component(tagForeground1ColorWheel)
|
||||
component(tagForeground2ColorWheel)
|
||||
}
|
||||
}
|
||||
row("Search highlight:") {
|
||||
cell {
|
||||
component(searchHighlightColorWheel)
|
||||
}
|
||||
}
|
||||
row("Editor fade opacity (%):") {
|
||||
medium(editorFadeOpacitySlider)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,21 +95,29 @@ internal class AceSettingsPanel {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -95,6 +132,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)
|
||||
|
||||
|
@@ -4,13 +4,25 @@ 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, 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(
|
||||
'+' 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");
|
||||
@@ -18,7 +30,7 @@ enum class KeyLayout(internal val rows: Array<String>, priority: String) {
|
||||
internal val allChars = rows.joinToString("").toCharArray().apply(CharArray::sort).joinToString("")
|
||||
internal 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 inline fun priority(crossinline tagToChar: (String) -> Char): (String) -> Int {
|
||||
return { allPriorities.getOrDefault(tagToChar(it), Int.MAX_VALUE) }
|
||||
}
|
||||
}
|
||||
|
@@ -7,13 +7,6 @@ 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].
|
||||
*/
|
||||
@@ -24,7 +17,7 @@ internal object KeyLayoutCache {
|
||||
* 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 (!::allPossibleTagsLowercase.isInitialized) {
|
||||
reset(settings)
|
||||
}
|
||||
}
|
||||
@@ -33,15 +26,16 @@ internal object KeyLayoutCache {
|
||||
* Re-initializes cached data according to updated settings.
|
||||
*/
|
||||
fun reset(settings: AceSettings) {
|
||||
tagOrder = compareBy(
|
||||
String::length,
|
||||
settings.layout.priority(String::last)
|
||||
)
|
||||
|
||||
@Suppress("ConvertLambdaToReference")
|
||||
val allSuffixChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars).toList() }
|
||||
val allPrefixChars = processCharList(settings.prefixChars).filterNot(allSuffixChars::contains).plus("")
|
||||
|
||||
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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,42 @@ 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() {
|
||||
class Literal(override val rawText: String, val excludeMiddlesOfWords: Boolean) : SearchQuery() {
|
||||
init {
|
||||
require(rawText.isNotEmpty())
|
||||
}
|
||||
|
||||
override fun refine(char: Char): SearchQuery {
|
||||
return Literal(rawText + char, excludeMiddlesOfWords)
|
||||
}
|
||||
|
||||
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()) {
|
||||
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)
|
||||
}
|
||||
|
||||
return Regex(Regex.escape(rawText), options)
|
||||
return Regex(pattern, setOf(RegexOption.MULTILINE))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +69,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(), excludeMiddlesOfWords = false)
|
||||
}
|
||||
|
||||
override fun getHighlightLength(text: CharSequence, offset: Int): Int {
|
||||
return 1
|
||||
}
|
||||
|
@@ -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
|
||||
|
||||
/**
|
||||
@@ -92,26 +89,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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
|
@@ -2,7 +2,9 @@ package org.acejump.session
|
||||
|
||||
import com.intellij.openapi.editor.Editor
|
||||
import org.acejump.boundaries.Boundaries
|
||||
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 +17,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(), excludeMiddlesOfWords = true), defaultBoundary)
|
||||
|
||||
return if (searchProcessor.isQueryFinished) {
|
||||
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
|
||||
@@ -49,8 +51,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 +61,16 @@ 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 && query.excludeMiddlesOfWords) {
|
||||
val newQuery = SearchQuery.Literal(query.rawText, excludeMiddlesOfWords = false)
|
||||
val newSearchProcessor = SearchProcessor(jumpEditors, newQuery, searchProcessor.boundaries)
|
||||
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 -> {
|
||||
|
@@ -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()) {
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
Reference in New Issue
Block a user