diff --git a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightersRecycler.java b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightersRecycler.java index 7aa079917182..c4549b89c089 100644 --- a/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightersRecycler.java +++ b/platform/analysis-impl/src/com/intellij/codeInsight/daemon/impl/HighlightersRecycler.java @@ -2,28 +2,36 @@ package com.intellij.codeInsight.daemon.impl; import com.intellij.openapi.editor.ex.RangeHighlighterEx; +import com.intellij.openapi.editor.impl.RangeMarkerImpl; import com.intellij.openapi.editor.markup.RangeHighlighter; import com.intellij.openapi.util.Key; -import com.intellij.openapi.util.ProperTextRange; -import com.intellij.openapi.util.TextRange; +import com.intellij.openapi.util.TextRangeScalarUtil; import com.intellij.openapi.util.UserDataHolderEx; -import com.intellij.util.containers.MultiMap; +import com.intellij.util.containers.ContainerUtil; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.ArrayList; import java.util.Collection; +import java.util.List; -// cache for highlighters not needed anymore. -// You call recycleHighlighter() to put unused highlighter into the cache -// and then call pickupHighlighterFromGarbageBin() (if there is a sudden need for fresh highlighter with specified offsets) to remove it from the cache to re-initialize and use. +/** + * Cache for highlighters scheduled for removal. + * You call {@link #recycleHighlighter} to put unused highlighter into the cache + * and then call {@link #pickupHighlighterFromGarbageBin} (if there is a sudden need for fresh highlighter with specified offsets) to remove it from the cache to re-initialize and re-use. + * In the end, call {@link UpdateHighlightersUtil#incinerateObsoleteHighlighters} to finally remove highlighters left in the cache that nobody picked up and reused. + */ final class HighlightersRecycler { - private final MultiMap incinerator = new MultiMap<>(); + private final Long2ObjectMap> incinerator = new Long2ObjectOpenHashMap<>(); private static final Key BEING_RECYCLED_KEY = Key.create("RECYCLED_KEY"); // set when the highlighter is just recycled, but not yet transferred to EDT to change its attributes. used to prevent double recycling the same RH // return true if RH is successfully recycled, false if race condition intervened boolean recycleHighlighter(@NotNull RangeHighlighterEx highlighter) { if (highlighter.isValid() && ((UserDataHolderEx)highlighter).replace(BEING_RECYCLED_KEY, null, Boolean.TRUE)) { - incinerator.putValue(ProperTextRange.create(highlighter), highlighter); + long range = ((RangeMarkerImpl)highlighter).getScalarRange(); + incinerator.computeIfAbsent(range, __ -> new ArrayList<>()).add(highlighter); return true; } return false; @@ -31,13 +39,19 @@ final class HighlightersRecycler { @Nullable // null means no highlighter found in the cache RangeHighlighterEx pickupHighlighterFromGarbageBin(int startOffset, int endOffset, int layer) { - TextRange range = new TextRange(startOffset, endOffset); - Collection collection = incinerator.get(range); - for (RangeHighlighterEx highlighter : collection) { - if (highlighter.isValid() && highlighter.getLayer() == layer) { - incinerator.remove(range, highlighter); - highlighter.putUserData(BEING_RECYCLED_KEY, null); - return highlighter; + long range = TextRangeScalarUtil.toScalarRange(startOffset, endOffset); + List collection = incinerator.get(range); + if (collection != null) { + for (int i = 0; i < collection.size(); i++) { + RangeHighlighterEx highlighter = collection.get(i); + if (highlighter.isValid() && highlighter.getLayer() == layer) { + collection.remove(i); + if (collection.isEmpty()) { + incinerator.remove(range); + } + highlighter.putUserData(BEING_RECYCLED_KEY, null); + return highlighter; + } } } return null; @@ -45,7 +59,7 @@ final class HighlightersRecycler { @NotNull Collection forAllInGarbageBin() { - return incinerator.values(); + return ContainerUtil.flatten(incinerator.values()); } // mark all remaining highlighters as not "recycled", to avoid double creation @@ -54,7 +68,8 @@ final class HighlightersRecycler { ((UserDataHolderEx)highlighter).replace(BEING_RECYCLED_KEY, Boolean.TRUE, null); } } - static boolean isBeingRecycled(RangeHighlighter highlighter) { + + static boolean isBeingRecycled(@NotNull RangeHighlighter highlighter) { return highlighter.getUserData(BEING_RECYCLED_KEY) != null; } } diff --git a/platform/core-impl/src/com/intellij/openapi/editor/impl/RangeMarkerImpl.java b/platform/core-impl/src/com/intellij/openapi/editor/impl/RangeMarkerImpl.java index 34e438445e43..9870ae7357be 100644 --- a/platform/core-impl/src/com/intellij/openapi/editor/impl/RangeMarkerImpl.java +++ b/platform/core-impl/src/com/intellij/openapi/editor/impl/RangeMarkerImpl.java @@ -19,6 +19,7 @@ import com.intellij.openapi.vfs.VirtualFile; import com.intellij.util.DocumentUtil; import com.intellij.util.ObjectUtils; import com.intellij.util.diff.FilesTooBigForDiffException; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; @@ -420,6 +421,21 @@ public class RangeMarkerImpl extends UserDataHolderBase implements RangeMarkerEx return node.intervalEnd(); } + /** + * @return this marker text range in the scalar form + */ + @ApiStatus.Internal + public long getScalarRange() { + RangeMarkerTree.RMNode node = myNode; + if (node == null) { + return -1; + } + long range = node.toScalarRange(); + int delta = node.computeDeltaUpToRoot(); + return TextRangeScalarUtil.shift(range, delta, delta); + } + + // return intrinsic range belonging to that node (without delta-up-to-the-root correction) long toScalarRange() { RangeMarkerTree.RMNode node = myNode; if (node == null) {