1
0
mirror of https://github.com/chylex/IntelliJ-AceJump.git synced 2025-04-24 15:15:42 +02:00

Optimize screen visibility checks during search

When 'Search whole file' is disabled, 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.
This commit is contained in:
chylex 2024-09-05 01:08:08 +02:00
parent efc81b8fde
commit 04c1519261
Signed by: chylex
GPG Key ID: 4DE42C8F19A80548
3 changed files with 39 additions and 20 deletions
src/main/kotlin/org/acejump

View File

@ -21,6 +21,11 @@ sealed class EditorOffsetCache {
*/
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.
*/
@ -35,6 +40,7 @@ sealed class EditorOffsetCache {
private class Cache: EditorOffsetCache() {
private var visibleArea: Pair<Point, Point>? = null
private val lineToVisibleOffsetRange = Int2ObjectOpenHashMap<IntRange>()
private val pointToOffset =
Object2IntOpenHashMap<Point>().apply { defaultReturnValue(-1) }
private val offsetToPoint = Int2ObjectOpenHashMap<Point>()
@ -42,6 +48,24 @@ sealed class EditorOffsetCache {
override fun visibleArea(editor: Editor): Pair<Point, Point> =
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 =
pointToOffset.getInt(pos).let { offset ->
if (offset != -1) offset
@ -64,6 +88,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 =
read { editor.logicalPositionToOffset(editor.xyToLogicalPosition(pos)) }

View File

@ -21,22 +21,7 @@ enum class StandardBoundaries : Boundaries {
}
override fun isOffsetInside(editor: Editor, offset: Int, cache: EditorOffsetCache): Boolean {
// 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
return cache.isVisible(editor, offset)
}
},

View File

@ -3,6 +3,7 @@ package org.acejump.search
import com.intellij.openapi.editor.Editor
import it.unimi.dsi.fastutil.ints.IntArrayList
import org.acejump.boundaries.Boundaries
import org.acejump.boundaries.EditorOffsetCache
import org.acejump.immutableText
import org.acejump.isWordPart
import org.acejump.matchesAt
@ -12,7 +13,6 @@ import org.acejump.matchesAt
* previous results when the user [type]s a character.
*/
internal class SearchProcessor private constructor(
private val editors: List<Editor>,
query: SearchQuery,
results: MutableMap<Editor, IntArrayList>
) {
@ -24,14 +24,15 @@ internal class SearchProcessor private constructor(
SearchProcessor(editors, SearchQuery.RegularExpression(pattern), boundaries)
}
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(editors, query, mutableMapOf()) {
private constructor(editors: List<Editor>, query: SearchQuery, boundaries: Boundaries) : this(query, mutableMapOf()) {
val regex = query.toRegex()
if (regex != null) {
for (editor in editors) {
val cache = EditorOffsetCache.new()
val offsets = IntArrayList()
val offsetRange = boundaries.getOffsetRange(editor)
val offsetRange = boundaries.getOffsetRange(editor, cache)
var result = regex.find(editor.immutableText, offsetRange.first)
while (result != null) {
@ -43,7 +44,7 @@ internal class SearchProcessor private constructor(
if (highlightEnd > offsetRange.last) {
break
}
else if (boundaries.isOffsetInside(editor, index)) {
else if (boundaries.isOffsetInside(editor, index, cache)) {
offsets.add(index)
}