mirror of
https://github.com/chylex/IntelliJ-AceJump.git
synced 2025-09-15 14:32:08 +02:00
Compare commits
31 Commits
b13d629046
...
vim
Author | SHA1 | Date | |
---|---|---|---|
568128f474
|
|||
018d29007c
|
|||
cb814e0999
|
|||
1fc06e8120
|
|||
841f2fd125
|
|||
6c8f19e311
|
|||
ce4f3b5f03
|
|||
46f42c88eb
|
|||
43dfec940e
|
|||
abe06ec7be
|
|||
3fc3cbc7f8
|
|||
5979579042
|
|||
ea61d49aa6
|
|||
dbc6db108d
|
|||
01c38df82a
|
|||
a3a86cf447
|
|||
59fbd4e19c
|
|||
6e08d56cdf
|
|||
9a14fb87e3
|
|||
d22bcc220e
|
|||
1f76a8bd25
|
|||
2e31ddd504
|
|||
47a34f6f14
|
|||
575283b2be
|
|||
8bb34d7f75
|
|||
8d1ef031d2
|
|||
a9375ec414
|
|||
b42112cc9e
|
|||
8e42c82821
|
|||
48bcf9f284
|
|||
2681d9901f
|
@@ -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-16"
|
version = "chylex-26"
|
||||||
|
|
||||||
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-41")
|
||||||
|
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 {
|
||||||
|
@@ -7,8 +7,6 @@ import com.intellij.openapi.project.Project
|
|||||||
import com.intellij.util.IncorrectOperationException
|
import com.intellij.util.IncorrectOperationException
|
||||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||||
|
|
||||||
annotation class ExternalUsage
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an immutable version of the currently edited document.
|
* Returns an immutable version of the currently edited document.
|
||||||
*/
|
*/
|
||||||
@@ -52,57 +50,6 @@ fun CharSequence.countMatchingCharacters(selfOffset: Int, otherText: String): In
|
|||||||
return i
|
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> {
|
fun MutableMap<Editor, IntArrayList>.clone(): MutableMap<Editor, IntArrayList> {
|
||||||
val clone = HashMap<Editor, IntArrayList>(size)
|
val clone = HashMap<Editor, IntArrayList>(size)
|
||||||
|
|
||||||
|
@@ -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.OP_PENDING
|
||||||
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
|
if (vim.mode is OP_PENDING) {
|
||||||
if (commandState.isOperatorPending) {
|
val keyHandler = KeyHandler.getInstance()
|
||||||
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.addAction(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 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))
|
||||||
|
@@ -18,6 +18,11 @@ sealed class EditorOffsetCache {
|
|||||||
*/
|
*/
|
||||||
abstract fun visibleArea(editor: Editor): Pair<Point, Point>
|
abstract fun visibleArea(editor: Editor): Pair<Point, Point>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether the offset is in the visible area rectangle.
|
||||||
|
*/
|
||||||
|
abstract fun isVisible(editor: Editor, offset: Int): Boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the editor offset at the provided pixel coordinate.
|
* Returns the editor offset at the provided pixel coordinate.
|
||||||
*/
|
*/
|
||||||
@@ -36,6 +41,7 @@ sealed class EditorOffsetCache {
|
|||||||
|
|
||||||
private class Cache : EditorOffsetCache() {
|
private class Cache : EditorOffsetCache() {
|
||||||
private var visibleArea: Pair<Point, Point>? = null
|
private var visibleArea: Pair<Point, Point>? = null
|
||||||
|
private val lineToVisibleOffsetRange = Int2ObjectOpenHashMap<IntRange>()
|
||||||
private val pointToOffset = Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
|
private val pointToOffset = Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
|
||||||
private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
|
private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
|
||||||
|
|
||||||
@@ -43,6 +49,24 @@ sealed class EditorOffsetCache {
|
|||||||
return visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
|
return visibleArea ?: Uncached.visibleArea(editor).also { visibleArea = it }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isVisible(editor: Editor, offset: Int): Boolean {
|
||||||
|
val visualLine = editor.offsetToVisualLine(offset, false)
|
||||||
|
|
||||||
|
var visibleRange = lineToVisibleOffsetRange.get(visualLine)
|
||||||
|
if (visibleRange == null) {
|
||||||
|
val (topLeft, bottomRight) = visibleArea(editor)
|
||||||
|
val lineY = editor.visualLineToY(visualLine)
|
||||||
|
|
||||||
|
val firstVisibleOffset = xyToOffset(editor, Point(topLeft.x, lineY))
|
||||||
|
val lastVisibleOffset = xyToOffset(editor, Point(bottomRight.x, lineY))
|
||||||
|
|
||||||
|
visibleRange = firstVisibleOffset..lastVisibleOffset
|
||||||
|
lineToVisibleOffsetRange.put(visualLine, visibleRange)
|
||||||
|
}
|
||||||
|
|
||||||
|
return offset in visibleRange
|
||||||
|
}
|
||||||
|
|
||||||
override fun xyToOffset(editor: Editor, pos: Point): Int {
|
override fun xyToOffset(editor: Editor, pos: Point): Int {
|
||||||
val offset = pointToOffset.getInt(pos)
|
val offset = pointToOffset.getInt(pos)
|
||||||
|
|
||||||
@@ -51,7 +75,6 @@ sealed class EditorOffsetCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Uncached.xyToOffset(editor, pos).also {
|
return Uncached.xyToOffset(editor, pos).also {
|
||||||
@Suppress("ReplacePutWithAssignment")
|
|
||||||
pointToOffset.put(pos, it)
|
pointToOffset.put(pos, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,7 +87,6 @@ sealed class EditorOffsetCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Uncached.offsetToXY(editor, offset).also {
|
return Uncached.offsetToXY(editor, offset).also {
|
||||||
@Suppress("ReplacePutWithAssignment")
|
|
||||||
offsetToPoint.put(offset, it)
|
offsetToPoint.put(offset, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,6 +102,15 @@ sealed class EditorOffsetCache {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun isVisible(editor: Editor, offset: Int): Boolean {
|
||||||
|
val (topLeft, bottomRight) = visibleArea(editor)
|
||||||
|
val pos = offsetToXY(editor, offset)
|
||||||
|
val x = pos.x
|
||||||
|
val y = pos.y
|
||||||
|
|
||||||
|
return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
|
||||||
|
}
|
||||||
|
|
||||||
override fun xyToOffset(editor: Editor, pos: Point): Int {
|
override fun xyToOffset(editor: Editor, pos: Point): Int {
|
||||||
return editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos))
|
return editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos))
|
||||||
}
|
}
|
||||||
|
@@ -13,23 +13,7 @@ enum class StandardBoundaries : Boundaries {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
|
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
|
||||||
|
return cache.isVisible(editor, offset)
|
||||||
// If we are not using a cache, calling getOffsetRange will cause additional 1-2 pixel coordinate -> offset lookups, which is a lot
|
|
||||||
// more expensive than one lookup compared against the visible area.
|
|
||||||
|
|
||||||
// However, if we are using a cache, it's likely that the topmost and bottommost positions are already cached whereas the provided
|
|
||||||
// offset isn't, so we save a lookup for every offset outside the range.
|
|
||||||
|
|
||||||
if (cache !== EditorOffsetCache.Uncached && offset !in getOffsetRange(editor, cache)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
val (topLeft, bottomRight) = cache.visibleArea(editor)
|
|
||||||
val pos = cache.offsetToXY(editor, offset)
|
|
||||||
val x = pos.x
|
|
||||||
val y = pos.y
|
|
||||||
|
|
||||||
return x >= topLeft.x && y >= topLeft.y && x <= bottomRight.x && y <= bottomRight.y
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@@ -20,6 +20,8 @@ class AceConfig : PersistentStateComponent<AceSettings> {
|
|||||||
|
|
||||||
val layout get() = settings.layout
|
val layout get() = settings.layout
|
||||||
val minQueryLength get() = settings.minQueryLength
|
val minQueryLength get() = settings.minQueryLength
|
||||||
|
val invertUppercaseMode get() = settings.invertUppercaseMode
|
||||||
|
val editorFadeOpacity get() = settings.editorFadeOpacity
|
||||||
val jumpModeColor get() = settings.jumpModeColor
|
val jumpModeColor get() = settings.jumpModeColor
|
||||||
val tagForegroundColor1 get() = settings.tagForegroundColor1
|
val tagForegroundColor1 get() = settings.tagForegroundColor1
|
||||||
val tagForegroundColor2 get() = settings.tagForegroundColor2
|
val tagForegroundColor2 get() = settings.tagForegroundColor2
|
||||||
|
@@ -13,9 +13,10 @@ 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.invertUppercaseMode != settings.invertUppercaseMode ||
|
||||||
|
panel.editorFadeOpacityPercent != settings.editorFadeOpacity ||
|
||||||
panel.jumpModeColor != settings.jumpModeColor ||
|
panel.jumpModeColor != settings.jumpModeColor ||
|
||||||
panel.tagForegroundColor1 != settings.tagForegroundColor1 ||
|
panel.tagForegroundColor1 != settings.tagForegroundColor1 ||
|
||||||
panel.tagForegroundColor2 != settings.tagForegroundColor2 ||
|
panel.tagForegroundColor2 != settings.tagForegroundColor2 ||
|
||||||
@@ -23,9 +24,10 @@ 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.invertUppercaseMode = panel.invertUppercaseMode
|
||||||
|
settings.editorFadeOpacity = panel.editorFadeOpacityPercent
|
||||||
panel.jumpModeColor?.let { settings.jumpModeColor = it }
|
panel.jumpModeColor?.let { settings.jumpModeColor = it }
|
||||||
panel.tagForegroundColor1?.let { settings.tagForegroundColor1 = it }
|
panel.tagForegroundColor1?.let { settings.tagForegroundColor1 = it }
|
||||||
panel.tagForegroundColor2?.let { settings.tagForegroundColor2 = it }
|
panel.tagForegroundColor2?.let { settings.tagForegroundColor2 = it }
|
||||||
|
@@ -8,8 +8,9 @@ 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 invertUppercaseMode: Boolean = false,
|
||||||
var minQueryLength: Int = 1,
|
var minQueryLength: Int = 1,
|
||||||
|
var editorFadeOpacity: Int = 70,
|
||||||
|
|
||||||
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
|
@OptionTag("jumpModeRGB", converter = ColorConverter::class)
|
||||||
var jumpModeColor: Color = Color(0xFFFFFF),
|
var jumpModeColor: Color = Color(0xFFFFFF),
|
||||||
|
@@ -2,18 +2,23 @@ package org.acejump.config
|
|||||||
|
|
||||||
import com.intellij.openapi.ui.ComboBox
|
import com.intellij.openapi.ui.ComboBox
|
||||||
import com.intellij.ui.ColorPanel
|
import com.intellij.ui.ColorPanel
|
||||||
|
import com.intellij.ui.components.JBCheckBox
|
||||||
|
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.Dimension
|
||||||
import java.awt.Font
|
import java.awt.Font
|
||||||
|
import java.util.Hashtable
|
||||||
import javax.swing.JCheckBox
|
import javax.swing.JCheckBox
|
||||||
import javax.swing.JComponent
|
import javax.swing.JLabel
|
||||||
import javax.swing.JPanel
|
import javax.swing.JPanel
|
||||||
|
import javax.swing.JSlider
|
||||||
import javax.swing.text.JTextComponent
|
import javax.swing.text.JTextComponent
|
||||||
import kotlin.reflect.KProperty
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
@@ -23,10 +28,19 @@ 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()
|
||||||
|
private val invertUppercaseModeCheckBox = JBCheckBox("Invert uppercase mode")
|
||||||
|
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
|
||||||
|
minimumSize = Dimension(275, minimumSize.height)
|
||||||
|
}
|
||||||
private val jumpModeColorWheel = ColorPanel()
|
private val jumpModeColorWheel = ColorPanel()
|
||||||
private val tagForeground1ColorWheel = ColorPanel()
|
private val tagForeground1ColorWheel = ColorPanel()
|
||||||
private val tagForeground2ColorWheel = ColorPanel()
|
private val tagForeground2ColorWheel = ColorPanel()
|
||||||
@@ -34,52 +48,46 @@ 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) }
|
||||||
|
row { cell(invertUppercaseModeCheckBox) }
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (%):") {
|
||||||
|
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
|
||||||
|
internal var invertUppercaseMode by invertUppercaseModeCheckBox
|
||||||
|
internal var editorFadeOpacity by editorFadeOpacitySlider
|
||||||
internal var jumpModeColor by jumpModeColorWheel
|
internal var jumpModeColor by jumpModeColorWheel
|
||||||
internal var tagForegroundColor1 by tagForeground1ColorWheel
|
internal var tagForegroundColor1 by tagForeground1ColorWheel
|
||||||
internal var tagForegroundColor2 by tagForeground2ColorWheel
|
internal var tagForegroundColor2 by tagForeground2ColorWheel
|
||||||
@@ -89,11 +97,16 @@ internal class AceSettingsPanel {
|
|||||||
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
|
get() = minQueryLength.toIntOrNull()?.coerceIn(1, 10)
|
||||||
set(value) { minQueryLength = value.toString() }
|
set(value) { minQueryLength = value.toString() }
|
||||||
|
|
||||||
|
internal var editorFadeOpacityPercent
|
||||||
|
get() = editorFadeOpacity * 10
|
||||||
|
set(value) { editorFadeOpacity = value / 10 }
|
||||||
|
|
||||||
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()
|
||||||
|
invertUppercaseMode = settings.invertUppercaseMode
|
||||||
|
editorFadeOpacityPercent = settings.editorFadeOpacity
|
||||||
jumpModeColor = settings.jumpModeColor
|
jumpModeColor = settings.jumpModeColor
|
||||||
tagForegroundColor1 = settings.tagForegroundColor1
|
tagForegroundColor1 = settings.tagForegroundColor1
|
||||||
tagForegroundColor2 = settings.tagForegroundColor2
|
tagForegroundColor2 = settings.tagForegroundColor2
|
||||||
@@ -102,7 +115,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
|
||||||
@@ -111,6 +124,9 @@ internal class AceSettingsPanel {
|
|||||||
private operator fun JCheckBox.getValue(a: AceSettingsPanel, p: KProperty<*>) = isSelected
|
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 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>.getValue(a: AceSettingsPanel, p: KProperty<*>) = selectedItem as T
|
||||||
private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item)
|
private operator fun <T> ComboBox<T>.setValue(a: AceSettingsPanel, p: KProperty<*>, item: T) = setSelectedItem(item)
|
||||||
|
|
||||||
|
@@ -10,7 +10,6 @@ import com.intellij.openapi.editor.actionSystem.TypedActionHandler
|
|||||||
* sessions' own handlers.
|
* sessions' own handlers.
|
||||||
*/
|
*/
|
||||||
internal object EditorKeyListener : TypedActionHandler {
|
internal object EditorKeyListener : TypedActionHandler {
|
||||||
private val action = TypedAction.getInstance()
|
|
||||||
private val attached = mutableMapOf<Editor, TypedActionHandler>()
|
private val attached = mutableMapOf<Editor, TypedActionHandler>()
|
||||||
private var originalHandler: TypedActionHandler? = null
|
private var originalHandler: TypedActionHandler? = null
|
||||||
|
|
||||||
@@ -20,8 +19,9 @@ internal object EditorKeyListener : TypedActionHandler {
|
|||||||
|
|
||||||
fun attach(editor: Editor, callback: TypedActionHandler) {
|
fun attach(editor: Editor, callback: TypedActionHandler) {
|
||||||
if (attached.isEmpty()) {
|
if (attached.isEmpty()) {
|
||||||
originalHandler = action.rawHandler
|
val typedAction = TypedAction.getInstance()
|
||||||
action.setupRawHandler(this)
|
originalHandler = typedAction.rawHandler
|
||||||
|
typedAction.setupRawHandler(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
attached[editor] = callback
|
attached[editor] = callback
|
||||||
@@ -31,7 +31,7 @@ internal object EditorKeyListener : TypedActionHandler {
|
|||||||
attached.remove(editor)
|
attached.remove(editor)
|
||||||
|
|
||||||
if (attached.isEmpty()) {
|
if (attached.isEmpty()) {
|
||||||
originalHandler?.let(action::setupRawHandler)
|
originalHandler?.let(TypedAction.getInstance()::setupRawHandler)
|
||||||
originalHandler = null
|
originalHandler = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -4,21 +4,49 @@ 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
|
* 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.
|
* ergonomically difficult they are to press.
|
||||||
*/
|
*/
|
||||||
@Suppress("unused")
|
@Suppress("unused", "SpellCheckingInspection")
|
||||||
enum class KeyLayout(internal val rows: Array<String>, priority: String) {
|
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", characterSides = sides(listOf("123456", "qwert", "asdfg", "zxcvb"), listOf("7890", "yuiop", "hjkl", "nm"))),
|
||||||
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210"),
|
QWERTZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "yxcvb"), listOf("7890", "zuiop", "hjkl", "nm"))),
|
||||||
|
QWERTZ_CZ(arrayOf("1234567890", "qwertzuiop", "asdfghjkl", "yxcvbnm"), priority = "fjghdkslavncmbxyrutzeiwoqp5849673210", characterSides = sides(listOf("123456", "qwert", "asdfg", "yxcvb"), listOf("7890", "zuiop", "hjkl", "nm")), 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"),
|
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 {
|
fun priority(char: Char): Int {
|
||||||
return { allPriorities.getOrDefault(tagToChar(it), Int.MAX_VALUE) }
|
return allPriorities[char] ?: allChars.length
|
||||||
|
}
|
||||||
|
|
||||||
|
fun areOnSameSide(c1: Char, c2: Char): Boolean {
|
||||||
|
return (c1 in characterSides.first && c2 in characterSides.first) || (c1 in characterSides.second && c2 in characterSides.second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun sides(left: List<String>, right: List<String>): Pair<Set<Char>, Set<Char>> {
|
||||||
|
return Pair(
|
||||||
|
left.flatMapTo(mutableSetOf()) { it.toCharArray().toSet() },
|
||||||
|
right.flatMapTo(mutableSetOf()) { it.toCharArray().toSet() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
@@ -1,30 +1,22 @@
|
|||||||
package org.acejump.input
|
package org.acejump.input
|
||||||
|
|
||||||
import org.acejump.config.AceSettings
|
import org.acejump.config.AceSettings
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores data specific to the selected keyboard layout. We want to assign tags with easily reachable keys first, and ideally have tags
|
* Stores data specific to the selected keyboard layout. We want to assign tags with easily reachable keys first, and ideally have tags
|
||||||
* 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 allowedTagsSorted: List<String>
|
||||||
* 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>
|
|
||||||
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 (!::tagOrder.isInitialized) {
|
if (!::allowedTagsSorted.isInitialized) {
|
||||||
reset(settings)
|
reset(settings)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,21 +25,38 @@ 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) {
|
||||||
tagOrder = compareBy(
|
val allowedChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars) }
|
||||||
String::length,
|
val allowedTags = mutableSetOf<String>()
|
||||||
settings.layout.priority(String::last)
|
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("ConvertLambdaToReference")
|
for (c1 in allowedChars) {
|
||||||
val allSuffixChars = processCharList(settings.allowedChars).ifEmpty { processCharList(settings.layout.allChars).toList() }
|
allowedTags.add("$c1")
|
||||||
val allPrefixChars = processCharList(settings.prefixChars).filterNot(allSuffixChars::contains).plus("")
|
|
||||||
|
|
||||||
allPossibleTagsLowercase = allSuffixChars
|
for (c2 in allowedChars) {
|
||||||
.flatMap { suffix -> allPrefixChars.map { prefix -> "$prefix$suffix" } }
|
if (c1 != c2) {
|
||||||
.sortedWith(tagOrder)
|
allowedTags.add("$c1$c2")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun processCharList(charList: String): Set<String> {
|
allowedTagsSorted = allowedTags.sortedBy { rankPriority(settings.layout, it) }
|
||||||
return charList.toCharArray().map(Char::lowercase).toSet()
|
}
|
||||||
|
|
||||||
|
private fun processCharList(charList: String): List<Char> {
|
||||||
|
return charList.toCharArray().map(Char::lowercaseChar).distinct()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rankPriority(layout: KeyLayout, tag: String): Int {
|
||||||
|
val c1 = tag.first()
|
||||||
|
val p1 = (1.0 + layout.priority(c1)).pow(3)
|
||||||
|
|
||||||
|
if (tag.length == 1) {
|
||||||
|
return p1.roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
val c2 = tag.last()
|
||||||
|
val p2 = (1.0 + layout.priority(c2)).pow(3)
|
||||||
|
|
||||||
|
val multiplier = if (layout.areOnSameSide(c1, c2)) 2 else 1
|
||||||
|
return (((p1 * 50) + p2 + 1000) * multiplier).roundToInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,6 +3,7 @@ package org.acejump.search
|
|||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import it.unimi.dsi.fastutil.ints.IntArrayList
|
import it.unimi.dsi.fastutil.ints.IntArrayList
|
||||||
import org.acejump.boundaries.Boundaries
|
import org.acejump.boundaries.Boundaries
|
||||||
|
import org.acejump.boundaries.EditorOffsetCache
|
||||||
import org.acejump.clone
|
import org.acejump.clone
|
||||||
import org.acejump.config.AceConfig
|
import org.acejump.config.AceConfig
|
||||||
import org.acejump.immutableText
|
import org.acejump.immutableText
|
||||||
@@ -11,25 +12,16 @@ 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, val invertUppercaseMode: Boolean, private val results: MutableMap<Editor, IntArrayList>) {
|
||||||
companion object {
|
internal constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries, invertUppercaseMode: Boolean) : this(query, boundaries, invertUppercaseMode, mutableMapOf()) {
|
||||||
fun fromString(editors: List<Editor>, query: String, boundaries: Boundaries): SearchProcessor {
|
val regex = query.toRegex(invertUppercaseMode)
|
||||||
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()
|
|
||||||
|
|
||||||
if (regex != null) {
|
if (regex != null) {
|
||||||
for (editor in editors) {
|
for (editor in editors) {
|
||||||
|
val cache = EditorOffsetCache.new()
|
||||||
val offsets = IntArrayList()
|
val offsets = IntArrayList()
|
||||||
|
|
||||||
val offsetRange = boundaries.getOffsetRange(editor)
|
val offsetRange = boundaries.getOffsetRange(editor, cache)
|
||||||
var result = regex.find(editor.immutableText, offsetRange.first)
|
var result = regex.find(editor.immutableText, offsetRange.first)
|
||||||
|
|
||||||
while (result != null) {
|
while (result != null) {
|
||||||
@@ -39,7 +31,7 @@ class SearchProcessor private constructor(query: SearchQuery, private val result
|
|||||||
if (highlightEnd > offsetRange.last) {
|
if (highlightEnd > offsetRange.last) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
else if (boundaries.isOffsetInside(editor, index)) {
|
else if (boundaries.isOffsetInside(editor, index, cache) && !editor.foldingModel.isOffsetCollapsed(index)) {
|
||||||
offsets.add(index)
|
offsets.add(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +57,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
|
||||||
}
|
}
|
||||||
|
@@ -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.
|
||||||
*/
|
*/
|
||||||
@@ -16,32 +21,40 @@ internal sealed class SearchQuery {
|
|||||||
/**
|
/**
|
||||||
* Converts the query into a regular expression to find the initial matches.
|
* Converts the query into a regular expression to find the initial matches.
|
||||||
*/
|
*/
|
||||||
abstract fun toRegex(): Regex?
|
abstract fun toRegex(invertUppercaseMode: Boolean): 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(invertUppercaseMode: Boolean): Regex {
|
||||||
val options = mutableSetOf(RegexOption.MULTILINE)
|
val firstChar = rawText.first()
|
||||||
|
val pattern = if (firstChar.isLowerCase() xor invertUppercaseMode) {
|
||||||
if (rawText.first().isLowerCase()) {
|
val fullPattern = Regex.escape(rawText)
|
||||||
options.add(RegexOption.IGNORE_CASE)
|
"(?i)$fullPattern"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val firstCharUppercasePattern = Regex.escape(firstChar.uppercase())
|
||||||
|
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,11 +64,15 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun toRegex(): Regex {
|
override fun toRegex(invertUppercaseMode: Boolean): Regex {
|
||||||
return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
|
return Regex(pattern, setOf(RegexOption.MULTILINE, RegexOption.IGNORE_CASE))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,15 +3,13 @@ package org.acejump.search
|
|||||||
import com.google.common.collect.ArrayListMultimap
|
import com.google.common.collect.ArrayListMultimap
|
||||||
import com.intellij.openapi.editor.Editor
|
import com.intellij.openapi.editor.Editor
|
||||||
import it.unimi.dsi.fastutil.ints.IntList
|
import it.unimi.dsi.fastutil.ints.IntList
|
||||||
|
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap
|
||||||
import org.acejump.boundaries.EditorOffsetCache
|
import org.acejump.boundaries.EditorOffsetCache
|
||||||
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
|
import org.acejump.boundaries.StandardBoundaries.VISIBLE_ON_SCREEN
|
||||||
import org.acejump.immutableText
|
|
||||||
import org.acejump.input.KeyLayoutCache
|
import org.acejump.input.KeyLayoutCache
|
||||||
import org.acejump.isWordPart
|
|
||||||
import org.acejump.view.TagMarker
|
import org.acejump.view.TagMarker
|
||||||
import kotlin.collections.component1
|
import kotlin.collections.component1
|
||||||
import kotlin.collections.component2
|
import kotlin.collections.component2
|
||||||
import kotlin.math.max
|
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,7 +41,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.size).zip(tagSites).toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun type(char: Char): TaggingResult {
|
internal fun type(char: Char): TaggingResult {
|
||||||
@@ -64,6 +62,68 @@ class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
|
private fun generateTags(tagCount: Int): List<String> {
|
||||||
|
val allowedTagsSorted = KeyLayoutCache.allowedTagsSorted
|
||||||
|
val tags = mutableListOf<String>()
|
||||||
|
|
||||||
|
val containedSingleCharTags = mutableSetOf<Char>()
|
||||||
|
val blockedSingleCharTags = mutableSetOf<Char>()
|
||||||
|
val doubleCharTagCountsByFirstChar = Object2IntOpenHashMap<Char>()
|
||||||
|
|
||||||
|
for (tag in allowedTagsSorted) {
|
||||||
|
val firstChar = tag.first()
|
||||||
|
|
||||||
|
if (tag.length == 1) {
|
||||||
|
if (firstChar in blockedSingleCharTags) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
containedSingleCharTags.add(firstChar)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (containedSingleCharTags.remove(firstChar)) {
|
||||||
|
tags.remove(firstChar.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedSingleCharTags.add(firstChar)
|
||||||
|
doubleCharTagCountsByFirstChar.addTo(firstChar, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.add(tag)
|
||||||
|
|
||||||
|
if (tags.size >= tagCount) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In rare cases, the final tag list may contain a double character tag that is the only tag starting with its first character,
|
||||||
|
// so we replace it with the single character tag.
|
||||||
|
for (entry in doubleCharTagCountsByFirstChar.object2IntEntrySet()) {
|
||||||
|
if (entry.intValue != 1) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.removeAt(tags.indexOfFirst { it.first() == entry.key })
|
||||||
|
|
||||||
|
val tag = entry.key.toString()
|
||||||
|
var previousTagIndex = -1
|
||||||
|
|
||||||
|
// The implementation of searching where to place the single character tag is theoretically slow,
|
||||||
|
// but getting here is so rare it doesn't matter.
|
||||||
|
for (i in allowedTagsSorted.indexOf(tag) - 1 downTo 0) {
|
||||||
|
previousTagIndex = tags.indexOf(allowedTagsSorted[i])
|
||||||
|
|
||||||
|
if (previousTagIndex != -1) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tags.add(previousTagIndex + 1, tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
@@ -92,26 +152,23 @@ class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
|
|||||||
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
|
return@Comparator if (aEditorIndex < bEditorIndex) -1 else 1
|
||||||
}
|
}
|
||||||
|
|
||||||
val aIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(aEditor, a.offset, caches.getValue(aEditor))
|
val aCaches = caches.getValue(aEditor)
|
||||||
val bIsVisible = VISIBLE_ON_SCREEN.isOffsetInside(bEditor, b.offset, caches.getValue(bEditor))
|
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) {
|
if (aIsVisible != bIsVisible) {
|
||||||
// Sites in immediate view should come first.
|
// Sites in immediate view should come first.
|
||||||
return@Comparator if (aIsVisible) -1 else 1
|
return@Comparator if (aIsVisible) -1 else 1
|
||||||
}
|
}
|
||||||
|
|
||||||
val aIsNotWordStart = aEditor.immutableText[max(0, a.offset - 1)].isWordPart
|
val firstEditor = editorPriority[0]
|
||||||
val bIsNotWordStart = bEditor.immutableText[max(0, b.offset - 1)].isWordPart
|
val caretPosition = caches.getValue(firstEditor).offsetToXY(firstEditor, firstEditor.caretModel.offset)
|
||||||
if (aIsNotWordStart != bIsNotWordStart) {
|
|
||||||
// Ensure that the first letter of a word is prioritized for tagging.
|
|
||||||
return@Comparator if (bIsNotWordStart) -1 else 1
|
|
||||||
}
|
|
||||||
|
|
||||||
when {
|
val aDistance = aCaches.offsetToXY(aEditor, a.offset).distanceSq(caretPosition)
|
||||||
a.offset < b.offset -> -1
|
val bDistance = bCaches.offsetToXY(bEditor, b.offset).distanceSq(caretPosition)
|
||||||
a.offset > b.offset -> 1
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
return@Comparator aDistance.compareTo(bDistance)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -113,7 +113,7 @@ class Session(private val mainEditor: Editor, private val jumpEditors: List<Edit
|
|||||||
}
|
}
|
||||||
|
|
||||||
setMode(mode())
|
setMode(mode())
|
||||||
state = SessionState.WaitForKey(actions, jumpEditors, defaultBoundary)
|
state = SessionState.WaitForKey(actions, jumpEditors, defaultBoundary, AceConfig.invertUppercaseMode)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -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, AceConfig.invertUppercaseMode)
|
||||||
textHighlighter.renderOccurrences(processor.resultsCopy, processor.query)
|
textHighlighter.renderOccurrences(processor.resultsCopy, processor.query)
|
||||||
|
|
||||||
state = SessionState.SelectTag(actions, jumpEditors, processor)
|
state = SessionState.SelectTag(actions, jumpEditors, processor)
|
||||||
|
@@ -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
|
||||||
|
|
||||||
@@ -13,9 +16,10 @@ sealed interface SessionState {
|
|||||||
private val actions: SessionActions,
|
private val actions: SessionActions,
|
||||||
private val jumpEditors: List<Editor>,
|
private val jumpEditors: List<Editor>,
|
||||||
private val defaultBoundary: Boundaries,
|
private val defaultBoundary: Boundaries,
|
||||||
|
private val invertUppercaseMode: Boolean,
|
||||||
) : 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, invertUppercaseMode)
|
||||||
|
|
||||||
return if (searchProcessor.isQueryFinished) {
|
return if (searchProcessor.isQueryFinished) {
|
||||||
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
|
TypeResult.ChangeState(SelectTag(actions, jumpEditors, searchProcessor))
|
||||||
@@ -49,8 +53,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 +63,26 @@ 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, searchProcessor.invertUppercaseMode)
|
||||||
|
return TypeResult.ChangeState(SelectTag(actions, jumpEditors, newSearchProcessor))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (char == '\n') {
|
||||||
|
val newSearchProcessor = SearchProcessor(jumpEditors, searchProcessor.query, searchProcessor.boundaries, !searchProcessor.invertUppercaseMode)
|
||||||
|
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 -> {
|
||||||
|
@@ -5,6 +5,7 @@ import com.intellij.openapi.editor.Editor
|
|||||||
import com.intellij.ui.ColorUtil
|
import com.intellij.ui.ColorUtil
|
||||||
import org.acejump.boundaries.EditorOffsetCache
|
import org.acejump.boundaries.EditorOffsetCache
|
||||||
import org.acejump.boundaries.StandardBoundaries
|
import org.acejump.boundaries.StandardBoundaries
|
||||||
|
import org.acejump.config.AceConfig
|
||||||
import java.awt.Graphics
|
import java.awt.Graphics
|
||||||
import java.awt.Graphics2D
|
import java.awt.Graphics2D
|
||||||
import java.awt.Rectangle
|
import java.awt.Rectangle
|
||||||
@@ -53,7 +54,7 @@ internal class TagCanvas(private val editor: Editor) : JComponent() {
|
|||||||
return
|
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)
|
g.fillRect(0, 0, width - 1, height - 1)
|
||||||
|
|
||||||
if (markers.isEmpty()) {
|
if (markers.isEmpty()) {
|
||||||
|
@@ -12,10 +12,13 @@ import java.awt.Rectangle
|
|||||||
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
|
* Describes a 1 or 2 character shortcut that points to a specific character in the editor.
|
||||||
*/
|
*/
|
||||||
internal class TagMarker(
|
internal class TagMarker(
|
||||||
private val tag: CharArray,
|
private val firstChar: String,
|
||||||
|
private val secondChar: String,
|
||||||
val offset: Int
|
val offset: Int
|
||||||
) {
|
) {
|
||||||
private val length = tag.size
|
private constructor(tag: String, offset: Int) : this(tag.first().toString(), tag.drop(1), offset)
|
||||||
|
|
||||||
|
private val length = firstChar.length + secondChar.length
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
@@ -28,7 +31,7 @@ internal class TagMarker(
|
|||||||
* character ([typedTag]) matches the first [tag] character, only the second [tag] character is displayed.
|
* character ([typedTag]) matches the first [tag] character, only the second [tag] character is displayed.
|
||||||
*/
|
*/
|
||||||
fun create(tag: String, offset: Int, typedTag: String): TagMarker {
|
fun create(tag: String, offset: Int, typedTag: String): TagMarker {
|
||||||
return TagMarker(tag.drop(typedTag.length).toCharArray(), offset)
|
return TagMarker(tag.drop(typedTag.length), offset)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +68,11 @@ internal class TagMarker(
|
|||||||
|
|
||||||
g.font = font.tagFont
|
g.font = font.tagFont
|
||||||
g.color = font.foregroundColor1
|
g.color = font.foregroundColor1
|
||||||
g.drawChars(tag, 0, 1, x, y)
|
g.drawString(firstChar, x, y)
|
||||||
|
|
||||||
if (tag.size > 1) {
|
if (secondChar.isNotEmpty()) {
|
||||||
g.color = font.foregroundColor2
|
g.color = font.foregroundColor2
|
||||||
g.drawChars(tag, 1, length - 1, x + font.tagCharWidth, y)
|
g.drawString(secondChar, x + font.tagCharWidth, y)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user