1
0
mirror of https://github.com/chylex/IntelliJ-AceJump.git synced 2024-11-24 23:42:46 +01:00

Compare commits

..

6 Commits

Author SHA1 Message Date
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
7 changed files with 83 additions and 37 deletions

View File

@ -8,7 +8,7 @@ plugins {
} }
group = "org.acejump" group = "org.acejump"
version = "chylex-23" version = "chylex-25"
repositories { repositories {
mavenCentral() mavenCentral()
@ -18,7 +18,7 @@ intellij {
version.set("2024.2") version.set("2024.2")
updateSinceUntilBuild.set(false) updateSinceUntilBuild.set(false)
plugins.add("IdeaVIM:chylex-40") plugins.add("IdeaVIM:chylex-41")
plugins.add("com.intellij.classic.ui:242.20224.159") plugins.add("com.intellij.classic.ui:242.20224.159")
pluginsRepositories { pluginsRepositories {

View File

@ -16,7 +16,7 @@ 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.newapi.vim import com.maddyhome.idea.vim.newapi.vim
import com.maddyhome.idea.vim.state.mode.Mode 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,8 +52,8 @@ sealed class AceVimAction : DumbAwareAction() {
} }
else { else {
val vim = editor.vim val vim = editor.vim
val keyHandler = KeyHandler.getInstance() if (vim.mode is OP_PENDING) {
if (keyHandler.isOperatorPending(vim.mode, keyHandler.keyHandlerState)) { val keyHandler = KeyHandler.getInstance()
val key = keyHandler.keyHandlerState.commandBuilder.keys.singleOrNull()?.keyChar val key = keyHandler.keyHandlerState.commandBuilder.keys.singleOrNull()?.keyChar
keyHandler.fullReset(vim) keyHandler.fullReset(vim)
@ -71,10 +71,10 @@ sealed class AceVimAction : DumbAwareAction() {
if (action != null) { if (action != null) {
ApplicationManager.getApplication().invokeLater { ApplicationManager.getApplication().invokeLater {
WriteAction.run<Nothing> { WriteAction.run<Nothing> {
keyHandler.keyHandlerState.commandBuilder.pushCommandPart(action) keyHandler.keyHandlerState.commandBuilder.addAction(action)
val cmd = keyHandler.keyHandlerState.commandBuilder.buildCommand() val cmd = keyHandler.keyHandlerState.commandBuilder.buildCommand()
val operatorArguments = OperatorArguments(vim.mode is Mode.OP_PENDING, cmd.rawCount, injector.vimState.mode) val operatorArguments = OperatorArguments(vim.mode is OP_PENDING, cmd.rawCount, injector.vimState.mode)
injector.vimState.executingCommand = cmd injector.vimState.executingCommand = cmd
injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments) injector.actionExecutor.executeVimAction(vim, action, context, operatorArguments)

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

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

@ -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
@ -17,9 +18,10 @@ class SearchProcessor private constructor(query: SearchQuery, val boundaries: Bo
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) {
@ -29,7 +31,7 @@ class SearchProcessor private constructor(query: SearchQuery, val boundaries: Bo
if (highlightEnd > offsetRange.last) { if (highlightEnd > offsetRange.last) {
break break
} }
else if (boundaries.isOffsetInside(editor, index) && !editor.foldingModel.isOffsetCollapsed(index)) { else if (boundaries.isOffsetInside(editor, index, cache) && !editor.foldingModel.isOffsetCollapsed(index)) {
offsets.add(index) offsets.add(index)
} }

View File

@ -3,6 +3,7 @@ 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.input.KeyLayoutCache import org.acejump.input.KeyLayoutCache
@ -40,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 = generateTags(tagSites).zip(tagSites).toMap() tagMap = generateTags(tagSites.size).zip(tagSites).toMap()
} }
internal fun type(char: Char): TaggingResult { internal fun type(char: Char): TaggingResult {
@ -61,13 +62,15 @@ class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
} }
private companion object { private companion object {
private fun generateTags(tagSites: List<Tag>): List<String> { private fun generateTags(tagCount: Int): List<String> {
val allowedTagsSorted = KeyLayoutCache.allowedTagsSorted
val tags = mutableListOf<String>() val tags = mutableListOf<String>()
val containedSingleCharTags = mutableSetOf<Char>() val containedSingleCharTags = mutableSetOf<Char>()
val blockedSingleCharTags = mutableSetOf<Char>() val blockedSingleCharTags = mutableSetOf<Char>()
val doubleCharTagCountsByFirstChar = Object2IntOpenHashMap<Char>()
for (tag in KeyLayoutCache.allowedTagsSorted) { for (tag in allowedTagsSorted) {
val firstChar = tag.first() val firstChar = tag.first()
if (tag.length == 1) { if (tag.length == 1) {
@ -83,15 +86,41 @@ class Tagger(private val editors: List<Editor>, results: Map<Editor, IntList>) {
} }
blockedSingleCharTags.add(firstChar) blockedSingleCharTags.add(firstChar)
doubleCharTagCountsByFirstChar.addTo(firstChar, 1)
} }
tags.add(tag) tags.add(tag)
if (tags.size >= tagSites.size) { if (tags.size >= tagCount) {
return tags 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 return tags
} }