/*
* Copyright 2000-2009 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.completion;
import com.intellij.codeInsight.lookup.Classifier;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.statistics.StatisticsInfo;
import com.intellij.psi.statistics.StatisticsManager;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.JBIterable;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author peter
*/
public class StatisticsWeigher extends CompletionWeigher {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.StatisticsWeigher.LookupStatisticsWeigher");
private static final Key<StatisticsInfo> BASE_STATISTICS_INFO = Key.create("Base statistics info");
@Override
public Comparable weigh(@NotNull final LookupElement item, @NotNull final CompletionLocation location) {
throw new UnsupportedOperationException();
}
public static class LookupStatisticsWeigher extends Classifier<LookupElement> {
private final CompletionLocation myLocation;
private final Map<LookupElement, StatisticsComparable> myWeights = ContainerUtil.newIdentityHashMap();
private final Set<String> myStringsWithWeights = ContainerUtil.newTroveSet();
private final Set<LookupElement> myNoStats = ContainerUtil.newIdentityTroveSet();
public LookupStatisticsWeigher(CompletionLocation location, Classifier<LookupElement> next) {
super(next, "stats");
myLocation = location;
}
@Override
public void addElement(@NotNull LookupElement element, @NotNull ProcessingContext context) {
StatisticsInfo baseInfo = getBaseStatisticsInfo(element, myLocation);
int weight = weigh(baseInfo);
if (weight != 0) {
myWeights.put(element, new StatisticsComparable(weight, baseInfo));
myStringsWithWeights.add(element.getLookupString());
}
if (baseInfo == StatisticsInfo.EMPTY) {
myNoStats.add(element);
}
super.addElement(element, context);
}
@NotNull
@Override
public Iterable<LookupElement> classify(@NotNull Iterable<LookupElement> source, @NotNull final ProcessingContext context) {
List<LookupElement> initialList = getInitialNoStatElements(source, context);
Iterable<LookupElement> rest = withoutInitial(source, initialList);
Collection<List<LookupElement>> byWeight = buildMapByWeight(rest).descendingMap().values();
return JBIterable.from(initialList).append(JBIterable.from(byWeight).flatten(group -> myNext.classify(group, context)));
}
private static Iterable<LookupElement> withoutInitial(Iterable<LookupElement> allItems, List<LookupElement> initial) {
Set<LookupElement> initialSet = ContainerUtil.newIdentityTroveSet(initial);
return JBIterable.from(allItems).filter(element -> !initialSet.contains(element));
}
private List<LookupElement> getInitialNoStatElements(Iterable<LookupElement> source, ProcessingContext context) {
List<LookupElement> initialList = new ArrayList<>();
for (LookupElement next : myNext.classify(source, context)) {
if (myNoStats.contains(next)) {
initialList.add(next);
}
else {
break;
}
}
return initialList;
}
private TreeMap<Integer, List<LookupElement>> buildMapByWeight(Iterable<LookupElement> source) {
MultiMap<String, LookupElement> byName = MultiMap.create();
List<LookupElement> noStats = new ArrayList<>();
for (LookupElement element : source) {
String string = element.getLookupString();
if (myStringsWithWeights.contains(string)) {
byName.putValue(string, element);
} else {
noStats.add(element);
}
}
TreeMap<Integer, List<LookupElement>> map = new TreeMap<>();
map.put(0, noStats);
for (String s : byName.keySet()) {
List<LookupElement> group = (List<LookupElement>)byName.get(s);
Collections.sort(group, Comparator.comparing(this::getScalarWeight).reversed());
map.computeIfAbsent(getMaxWeight(group), __ -> new ArrayList<>()).addAll(group);
}
return map;
}
private int getMaxWeight(List<LookupElement> group) {
int max = 0;
//noinspection ForLoopReplaceableByForEach
for (int i = 0; i < group.size(); i++) {
max = Math.max(max, getScalarWeight(group.get(i)));
}
return max;
}
private int getScalarWeight(LookupElement e) {
StatisticsComparable comparable = myWeights.get(e);
return comparable == null ? 0 : comparable.getScalar();
}
private StatisticsComparable getWeight(LookupElement t) {
StatisticsComparable w = myWeights.get(t);
if (w == null) {
StatisticsInfo info = getBaseStatisticsInfo(t, myLocation);
myWeights.put(t, w = new StatisticsComparable(weigh(info), info));
}
return w;
}
private static int weigh(final StatisticsInfo baseInfo) {
if (baseInfo == StatisticsInfo.EMPTY) {
return 0;
}
int minRecency = baseInfo.getLastUseRecency();
return minRecency == Integer.MAX_VALUE ? 0 : StatisticsManager.RECENCY_OBLIVION_THRESHOLD - minRecency;
}
@NotNull
@Override
public List<Pair<LookupElement, Object>> getSortingWeights(@NotNull Iterable<LookupElement> items, @NotNull final ProcessingContext context) {
return ContainerUtil.map(items, lookupElement -> new Pair<LookupElement, Object>(lookupElement, getWeight(lookupElement)));
}
@Override
public void removeElement(@NotNull LookupElement element, @NotNull ProcessingContext context) {
myWeights.remove(element);
myNoStats.remove(element);
super.removeElement(element, context);
}
}
public static void clearBaseStatisticsInfo(LookupElement item) {
item.putUserData(BASE_STATISTICS_INFO, null);
}
@NotNull
public static StatisticsInfo getBaseStatisticsInfo(LookupElement item, @Nullable CompletionLocation location) {
StatisticsInfo info = BASE_STATISTICS_INFO.get(item);
if (info == null) {
if (location == null) {
return StatisticsInfo.EMPTY;
}
BASE_STATISTICS_INFO.set(item, info = calcBaseInfo(item, location));
}
return info;
}
@NotNull
private static StatisticsInfo calcBaseInfo(LookupElement item, @NotNull CompletionLocation location) {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
LOG.assertTrue(!ApplicationManager.getApplication().isDispatchThread());
}
StatisticsInfo info = StatisticsManager.serialize(CompletionService.STATISTICS_KEY, item, location);
return info == null ? StatisticsInfo.EMPTY : info;
}
}