2727import org .springframework .stereotype .Component ;
2828
2929import java .net .URI ;
30+ import java .util .Arrays ;
3031import java .util .Collections ;
32+ import java .util .Comparator ;
3133import java .util .Map ;
3234import java .util .Optional ;
3335import java .util .Set ;
4042@ Component
4143@ WorkspaceScope
4244public class LocationRepository {
45+
46+ private static final SymbolOccurrence [] EMPTY_OCCURRENCES = new SymbolOccurrence [0 ];
47+
48+ /**
49+ * Порядок вхождений по началу диапазона. Диапазоны вхождений непересекающиеся,
50+ * поэтому порядок тотален и однозначен.
51+ */
52+ private static final Comparator <SymbolOccurrence > BY_RANGE_START = Comparator
53+ .comparingInt ((SymbolOccurrence occurrence ) -> occurrence .location ().startLine ())
54+ .thenComparingInt (occurrence -> occurrence .location ().startCharacter ());
55+
4356 /**
4457 * Список обращений к символу, сгруппированный по URI.
4558 */
4659 private final Map <URI , Set <SymbolOccurrence >> locations = new ConcurrentHashMap <>();
4760
4861 /**
49- * Вторичный индекс вхождений по строке их расположения — для O(1)-поиска
50- * вхождения в позиции ({@link #findByPosition}) вместо линейного скана всего
51- * набора вхождений документа. На одной строке может быть несколько вхождений
52- * (разные колонки), поэтому значение — множество, разводимое по колонкам через
53- * {@link Ranges#containsPosition} в {@link #findByPosition}.
62+ * Вторичный индекс для {@link #findByPosition}: отсортированный по началу
63+ * диапазона снимок вхождений документа. Позволяет искать вхождение в позиции
64+ * бинарным поиском без построения карты «строка → множество вхождений»
65+ * (карта с per-строчными множествами на больших проектах доминировала в
66+ * потреблении памяти индексом ссылок).
67+ * <p>
68+ * Снимок ленивый: сбрасывается при любом изменении вхождений URI
69+ * ({@link #updateLocation}, {@link #delete}) и пересобирается при первом
70+ * следующем поиске.
5471 */
55- private final Map <URI , Map < Integer , Set < SymbolOccurrence >>> locationsByLine = new ConcurrentHashMap <>();
72+ private final Map <URI , SymbolOccurrence []> sortedOccurrences = new ConcurrentHashMap <>();
5673
5774 /**
5875 * Получить все обращения к символам в указанном URI.
@@ -65,30 +82,53 @@ public Stream<SymbolOccurrence> getSymbolOccurrencesByLocationUri(URI uri) {
6582 }
6683
6784 /**
68- * Найти вхождение к символу, чей диапазон накрывает позицию. O(1)-lookup по
69- * строке через {@link #locationsByLine}; при нескольких вхождениях на строке
70- * разводит по колонке через {@link Ranges#containsPosition}. Диапазоны
71- * вхождений непересекающиеся, поэтому совпадение максимум одно .
85+ * Найти вхождение к символу, чей диапазон накрывает позицию. Бинарный поиск
86+ * по отсортированному снимку вхождений документа: диапазоны вхождений
87+ * непересекающиеся, поэтому единственный кандидат — вхождение с наибольшим
88+ * началом диапазона, не превосходящим позицию .
7289 *
7390 * @param uri URI документа.
7491 * @param position позиция.
7592 * @return вхождение в позиции либо empty.
7693 */
7794 public Optional <SymbolOccurrence > findByPosition (URI uri , Position position ) {
78- var byLine = locationsByLine .get (uri );
79- if (byLine == null ) {
80- return Optional .empty ();
95+ var sorted = sortedOccurrences .computeIfAbsent (uri , this ::sortedSnapshot );
96+ var index = lastStartingAtOrBefore (sorted , position );
97+ if (index >= 0 && matches (sorted [index ], position )) {
98+ return Optional .of (sorted [index ]);
8199 }
82- var bucket = byLine .get (position .getLine ());
83- if (bucket == null ) {
84- return Optional .empty ();
85- }
86- for (var symbolOccurrence : bucket ) {
87- if (matches (symbolOccurrence , position )) {
88- return Optional .of (symbolOccurrence );
100+ return Optional .empty ();
101+ }
102+
103+ private SymbolOccurrence [] sortedSnapshot (URI uri ) {
104+ var snapshot = locations .getOrDefault (uri , Collections .emptySet ()).toArray (EMPTY_OCCURRENCES );
105+ Arrays .sort (snapshot , BY_RANGE_START );
106+ return snapshot ;
107+ }
108+
109+ /**
110+ * Индекс последнего вхождения, чей диапазон начинается не позже позиции,
111+ * либо {@code -1}, если все вхождения начинаются позже.
112+ */
113+ private static int lastStartingAtOrBefore (SymbolOccurrence [] sorted , Position position ) {
114+ var low = 0 ;
115+ var high = sorted .length - 1 ;
116+ var found = -1 ;
117+ while (low <= high ) {
118+ var mid = (low + high ) >>> 1 ;
119+ if (startsAfter (sorted [mid ].location (), position )) {
120+ high = mid - 1 ;
121+ } else {
122+ found = mid ;
123+ low = mid + 1 ;
89124 }
90125 }
91- return Optional .empty ();
126+ return found ;
127+ }
128+
129+ private static boolean startsAfter (Location location , Position position ) {
130+ return location .startLine () > position .getLine ()
131+ || (location .startLine () == position .getLine () && location .startCharacter () > position .getCharacter ());
92132 }
93133
94134 private static boolean matches (SymbolOccurrence symbolOccurrence , Position position ) {
@@ -107,10 +147,7 @@ public void updateLocation(SymbolOccurrence symbolOccurrence) {
107147 var location = symbolOccurrence .location ();
108148 locations .computeIfAbsent (location .uri (), uri -> ConcurrentHashMap .newKeySet ())
109149 .add (symbolOccurrence );
110- var byLine = locationsByLine .computeIfAbsent (location .uri (), uri -> new ConcurrentHashMap <>());
111- for (var line = location .startLine (); line <= location .endLine (); line ++) {
112- byLine .computeIfAbsent (line , l -> ConcurrentHashMap .newKeySet ()).add (symbolOccurrence );
113- }
150+ sortedOccurrences .remove (location .uri ());
114151 }
115152
116153 /**
@@ -120,6 +157,6 @@ public void updateLocation(SymbolOccurrence symbolOccurrence) {
120157 */
121158 public void delete (URI uri ) {
122159 locations .remove (uri );
123- locationsByLine .remove (uri );
160+ sortedOccurrences .remove (uri );
124161 }
125162}
0 commit comments