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:
parent
efc81b8fde
commit
04c1519261
src/main/kotlin/org/acejump
@ -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)) }
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user