blob: 5a93e653e0563c988c4429bea55cd2c0c87b86e4 [file] [log] [blame]
/*
* 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.codeInsight.lookup.WeighingContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Key;
import com.intellij.psi.statistics.StatisticsInfo;
import com.intellij.psi.statistics.StatisticsManager;
import com.intellij.util.ProcessingContext;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FlatteningIterator;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
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 Classifier<LookupElement> myNext;
private final Map<LookupElement, Integer> myWeights = new IdentityHashMap<LookupElement, Integer>();
@SuppressWarnings("unchecked") private final Set<LookupElement> myNoStats = new THashSet<LookupElement>(TObjectHashingStrategy.IDENTITY);
private int myPrefixChanges;
public LookupStatisticsWeigher(CompletionLocation location, Classifier<LookupElement> next) {
myLocation = location;
myNext = next;
}
@Override
public void addElement(LookupElement element, ProcessingContext context) {
StatisticsInfo baseInfo = getBaseStatisticsInfo(element, myLocation);
myWeights.put(element, weigh(element, baseInfo, context.get(CompletionLookupArranger.WEIGHING_CONTEXT)));
if (baseInfo == StatisticsInfo.EMPTY) {
myNoStats.add(element);
}
myNext.addElement(element, context);
}
private void checkPrefixChanged(ProcessingContext context) {
int actualPrefixChanges = context.get(CompletionLookupArranger.PREFIX_CHANGES).intValue();
if (myPrefixChanges != actualPrefixChanges) {
myPrefixChanges = actualPrefixChanges;
myWeights.clear();
}
}
@Override
public Iterable<LookupElement> classify(Iterable<LookupElement> source, final ProcessingContext context) {
checkPrefixChanged(context);
final Collection<List<LookupElement>> byWeight = buildMapByWeight(source, context).descendingMap().values();
List<LookupElement> initialList = getInitialNoStatElements(source, context);
//noinspection unchecked
final THashSet<LookupElement> initialSet = new THashSet<LookupElement>(initialList, TObjectHashingStrategy.IDENTITY);
final Condition<LookupElement> notInInitialList = new Condition<LookupElement>() {
@Override
public boolean value(LookupElement element) {
return !initialSet.contains(element);
}
};
return ContainerUtil.concat(initialList, new Iterable<LookupElement>() {
@Override
public Iterator<LookupElement> iterator() {
return new FlatteningIterator<List<LookupElement>, LookupElement>(byWeight.iterator()) {
@Override
protected Iterator<LookupElement> createValueIterator(List<LookupElement> group) {
return myNext.classify(ContainerUtil.findAll(group, notInInitialList), context).iterator();
}
};
}
});
}
private List<LookupElement> getInitialNoStatElements(Iterable<LookupElement> source, ProcessingContext context) {
List<LookupElement> initialList = new ArrayList<LookupElement>();
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, ProcessingContext context) {
TreeMap<Integer, List<LookupElement>> map = new TreeMap<Integer, List<LookupElement>>();
for (LookupElement element : source) {
final int weight = getWeight(element, context.get(CompletionLookupArranger.WEIGHING_CONTEXT));
List<LookupElement> list = map.get(weight);
if (list == null) {
map.put(weight, list = new SmartList<LookupElement>());
}
list.add(element);
}
return map;
}
private int getWeight(LookupElement t, WeighingContext context) {
Integer w = myWeights.get(t);
if (w == null) {
myWeights.put(t, w = weigh(t, getBaseStatisticsInfo(t, myLocation), context));
}
return w;
}
private static int weigh(@NotNull LookupElement item, final StatisticsInfo baseInfo, WeighingContext context) {
if (baseInfo == StatisticsInfo.EMPTY) {
return 0;
}
String prefix = context.itemPattern(item);
StatisticsInfo composed = composeStatsWithPrefix(baseInfo, prefix, false);
int minRecency = composed.getLastUseRecency();
int useCount = composed.getUseCount();
return minRecency == Integer.MAX_VALUE ? useCount : 100 - minRecency;
}
@Override
public void describeItems(LinkedHashMap<LookupElement, StringBuilder> map, ProcessingContext context) {
checkPrefixChanged(context);
for (LookupElement element : map.keySet()) {
StringBuilder builder = map.get(element);
if (builder.length() > 0) {
builder.append(", ");
}
builder.append("stats=").append(getWeight(element, context.get(CompletionLookupArranger.WEIGHING_CONTEXT)));
}
myNext.describeItems(map, 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;
}
/**
* For different prefixes we want to prefer different completion items,
* so we decorate their basic stat-infos depending on prefix.
* For example, consider that an item "fooBar" was chosen with a prefix "foo"
* Then we'll register "fooBar" for each of the sub-prefixes: "", "f", "fo" and "foo"
* and suggest "foobar" whenever we a user types any of those prefixes
*
* If a user has typed "fooB" for which there's no stat-info registered, we want to check
* all of its sub-prefixes: "", "f", "fo", "foo" and see if any of them is associated with a stat-info
* But if the item were "fobia" and the user has typed "fob", we don't want to claim
* that "fooBar" (which matches) is statistically better than "fobia" with prefix "fob" even though both begin with "fo"
* So we only check non-partial sub-prefixes, then ones that had been really typed by the user before completing
*
* @param forWriting controls whether this stat-info will be used for incrementing usage count or for its retrieval (for sorting)
*/
public static StatisticsInfo composeStatsWithPrefix(StatisticsInfo info, final String fullPrefix, boolean forWriting) {
ArrayList<StatisticsInfo> infos = new ArrayList<StatisticsInfo>((fullPrefix.length() + 3) * info.getConjuncts().size());
for (StatisticsInfo conjunct : info.getConjuncts()) {
if (forWriting) {
// some completion contributors may need pure statistical information to speed up searching for frequently chosen items
infos.add(conjunct);
}
for (int i = 0; i <= fullPrefix.length(); i++) {
// if we're incrementing usage count, register all sub-prefixes with "partial" mark
// if we're sorting and any sub-prefix was used as non-partial to choose this completion item, prefer it
infos.add(composeWithPrefix(conjunct, fullPrefix.substring(0, i), forWriting));
}
// if we're incrementing usage count, the full prefix is registered as non-partial
// if we're sorting and the current prefix was used as partial sub-prefix to choose this completion item, prefer it
infos.add(composeWithPrefix(conjunct, fullPrefix, !forWriting));
}
return StatisticsInfo.createComposite(infos);
}
private static StatisticsInfo composeWithPrefix(StatisticsInfo info, String fullPrefix, boolean partial) {
return new StatisticsInfo(info.getContext() + "###prefix=" + fullPrefix + "###part#" + partial, info.getValue());
}
}