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

31 Commits

Author SHA1 Message Date
568128f474 Set version to chylex-26 2024-10-31 03:57:33 +01:00
018d29007c Add option to invert behavior of holding Shift when searching, and toggle with Enter 2024-10-31 03:57:33 +01:00
cb814e0999 Set version to chylex-25 2024-09-05 09:11:05 +02:00
1fc06e8120 Update for latest IdeaVim 2024-09-05 09:10:21 +02:00
841f2fd125 Set version to chylex-24 2024-09-05 06:44:02 +02:00
6c8f19e311 Fix edge case where generated tags may have a double character tag that is the only tag starting with its first character 2024-09-05 06:43:41 +02:00
ce4f3b5f03 Fix IntelliJ SDK warning about services in class initializer 2024-09-05 02:44:39 +02:00
46f42c88eb Optimize screen visibility checks during search
Search results are tested for visibility on the screen. There is already an optimization that only checks results on visible lines, but the number of offset-to-XY conversions still scales linearly with the number of results, which can be very large in files with long lines.

A simple observation is that every line has a first and last offset that is visible on the screen (which may be different for each line due to proportional fonts).

This commit caches the visible offset range for every line involved in one search query, so testing visibility of a search result becomes a check if its offset is inside its line's visible offset range. Finding the visible offset range requires two XY-to-offset conversions per line, so the total number of conversions is bounded by the number of lines that can fit on the screen.

The worst case for this optimization is when every line has exactly one search result; before, this would lead to one offset-to-XY conversion per line, whereas now it leads to two XY-to-offset conversions per line. However, the maximum number of conversions is twice the number of visible lines, which will generally be very small.
2024-09-05 02:12:48 +02:00
43dfec940e Set version to chylex-23 2024-09-04 11:40:16 +02:00
abe06ec7be Increase width of editor fade opacity slider 2024-09-04 11:39:59 +02:00
3fc3cbc7f8 Optimize tagging 2024-09-04 11:33:26 +02:00
5979579042 Set version to chylex-22 2024-09-04 08:50:33 +02:00
ea61d49aa6 Refactor TagMarker to reduce allocations during rendering 2024-09-04 08:50:13 +02:00
dbc6db108d Redo tag generation (eliminate explicit prefix chars) 2024-09-04 08:44:52 +02:00
01c38df82a Search in the middle of words again unless pressing an uppercase letter, and rebind Space to cycling search boundaries 2024-09-03 21:50:03 +02:00
a3a86cf447 Update for IDEA 2024.2 2024-09-03 20:58:08 +02:00
59fbd4e19c Set version to chylex-21 2024-07-15 13:46:02 +02:00
6e08d56cdf Update for IDEA 2024.1 & latest IdeaVim 2024-07-15 13:45:39 +02:00
9a14fb87e3 Set version to chylex-20 2024-03-27 13:53:41 +01:00
d22bcc220e Update IdeaVim integration 2024-03-27 13:53:25 +01:00
1f76a8bd25 Set version to chylex-19 2023-12-29 21:45:12 +01:00
2e31ddd504 Use top row remapping only for tags 2023-12-29 21:45:12 +01:00
47a34f6f14 Disable searching in the middle of a word unless Space is pressed when typing tag 2023-12-29 21:45:12 +01:00
575283b2be Set version to chylex-18 2023-12-18 05:25:09 +01:00
8bb34d7f75 Make editor fade opacity configurable 2023-12-18 05:24:57 +01:00
8d1ef031d2 Set version to chylex-17 2023-12-18 03:44:20 +01:00
a9375ec414 Remove unused code 2023-12-18 03:44:01 +01:00
b42112cc9e Do not try to tag folded regions 2023-12-18 02:36:41 +01:00
8e42c82821 Use distance from current caret for tag order 2023-12-18 02:32:54 +01:00
48bcf9f284 Add QWERTZ (CZ) layout that remaps top row to digits 2023-12-18 00:58:37 +01:00
2681d9901f Change priority order of two-character tags 2023-12-18 00:45:28 +01:00
20 changed files with 338 additions and 218 deletions

View File

@@ -4,23 +4,26 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
kotlin("jvm") version "1.9.10" kotlin("jvm") version "1.9.10"
id("org.jetbrains.intellij") version "1.16.1" id("org.jetbrains.intellij") version "1.17.3"
} }
group = "org.acejump" group = "org.acejump"
version = "chylex-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 {

View File

@@ -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)

View File

@@ -1,5 +1,6 @@
package org.acejump.action package org.acejump.action
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.application.ApplicationManager import com.intellij.openapi.application.ApplicationManager
@@ -10,13 +11,12 @@ import com.maddyhome.idea.vim.action.change.change.ChangeVisualAction
import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction import com.maddyhome.idea.vim.action.change.delete.DeleteVisualAction
import com.maddyhome.idea.vim.action.copy.YankVisualAction import com.maddyhome.idea.vim.action.copy.YankVisualAction
import com.maddyhome.idea.vim.api.injector import com.maddyhome.idea.vim.api.injector
import com.maddyhome.idea.vim.command.MappingMode.OP_PENDING
import com.maddyhome.idea.vim.command.OperatorArguments import com.maddyhome.idea.vim.command.OperatorArguments
import com.maddyhome.idea.vim.group.visual.vimSetSelection import com.maddyhome.idea.vim.group.visual.vimSetSelection
import com.maddyhome.idea.vim.helper.inVisualMode import com.maddyhome.idea.vim.helper.inVisualMode
import com.maddyhome.idea.vim.helper.vimSelectionStart import com.maddyhome.idea.vim.helper.vimSelectionStart
import com.maddyhome.idea.vim.helper.vimStateMachine
import com.maddyhome.idea.vim.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode.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))

View File

@@ -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))
} }

View File

@@ -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
} }
}, },

View File

@@ -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

View File

@@ -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 }

View File

@@ -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),

View File

@@ -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)

View File

@@ -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
} }
} }

View File

@@ -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() }
)
}

View File

@@ -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("")
for (c2 in allowedChars) {
if (c1 != c2) {
allowedTags.add("$c1$c2")
}
}
}
allPossibleTagsLowercase = allSuffixChars allowedTagsSorted = allowedTags.sortedBy { rankPriority(settings.layout, it) }
.flatMap { suffix -> allPrefixChars.map { prefix -> "$prefix$suffix" } }
.sortedWith(tagOrder)
} }
private fun processCharList(charList: String): Set<String> { private fun processCharList(charList: String): List<Char> {
return charList.toCharArray().map(Char::lowercase).toSet() return charList.toCharArray().map(Char::lowercaseChar).distinct()
}
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()
} }
} }

View File

@@ -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
} }

View File

@@ -8,6 +8,11 @@ import org.acejump.countMatchingCharacters
internal sealed class SearchQuery { internal sealed class SearchQuery {
abstract val rawText: String abstract val rawText: String
/**
* Returns a new query with the given character appended.
*/
abstract fun refine(char: Char): SearchQuery
/** /**
* Returns how many characters the search occurrence highlight should cover. * Returns how many characters the search occurrence highlight should cover.
*/ */
@@ -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))
} }
} }

View File

@@ -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)
}
} }
} }
} }

View File

@@ -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)

View File

@@ -2,7 +2,10 @@ package org.acejump.session
import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.Editor
import org.acejump.boundaries.Boundaries import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.StandardBoundaries
import org.acejump.config.AceConfig
import org.acejump.search.SearchProcessor import org.acejump.search.SearchProcessor
import org.acejump.search.SearchQuery
import org.acejump.search.Tagger import org.acejump.search.Tagger
import org.acejump.search.TaggingResult import org.acejump.search.TaggingResult
@@ -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 -> {

View File

@@ -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()) {

View File

@@ -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)
} }
} }