blob: 1772ac77d45192ceea62bdb809e47ab5b4d63254 [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.completion.impl.CompletionServiceImpl;
import com.intellij.codeInsight.completion.impl.CompletionSorterImpl;
import com.intellij.codeInsight.lookup.*;
import com.intellij.codeInsight.lookup.impl.EmptyLookupItem;
import com.intellij.codeInsight.lookup.impl.LookupImpl;
import com.intellij.codeInsight.template.impl.LiveTemplateLookupElement;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.featureStatistics.FeatureUsageTrackerImpl;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.RangeMarker;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.statistics.StatisticsInfo;
import com.intellij.util.Alarm;
import com.intellij.util.ProcessingContext;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.util.*;
public class CompletionLookupArranger extends LookupArranger {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.CompletionLookupArranger");
@Nullable private static StatisticsUpdate ourPendingUpdate;
private static final Alarm ourStatsAlarm = new Alarm(ApplicationManager.getApplication());
private static final Key<String> PRESENTATION_INVARIANT = Key.create("PRESENTATION_INVARIANT");
private static final Comparator<LookupElement> BY_PRESENTATION_COMPARATOR = new Comparator<LookupElement>() {
@Override
public int compare(LookupElement o1, LookupElement o2) {
String invariant = PRESENTATION_INVARIANT.get(o1);
assert invariant != null;
return invariant.compareToIgnoreCase(PRESENTATION_INVARIANT.get(o2));
}
};
private static final int MAX_PREFERRED_COUNT = 5;
public static final Key<WeighingContext> WEIGHING_CONTEXT = Key.create("WEIGHING_CONTEXT");
public static final Key<Boolean> PURE_RELEVANCE = Key.create("PURE_RELEVANCE");
public static final Key<Integer> PREFIX_CHANGES = Key.create("PREFIX_CHANGES");
private static final UISettings ourUISettings = UISettings.getInstance();
private final List<LookupElement> myFrozenItems = new ArrayList<LookupElement>();
static {
Disposer.register(ApplicationManager.getApplication(), new Disposable() {
@Override
public void dispose() {
cancelLastCompletionStatisticsUpdate();
}
});
}
private final CompletionLocation myLocation;
private final CompletionParameters myParameters;
private final CompletionProgressIndicator myProcess;
@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
private final Map<CompletionSorterImpl, Classifier<LookupElement>> myClassifiers = new LinkedHashMap<CompletionSorterImpl, Classifier<LookupElement>>();
private int myPrefixChanges;
public CompletionLookupArranger(final CompletionParameters parameters, CompletionProgressIndicator process) {
myParameters = parameters;
myProcess = process;
myLocation = new CompletionLocation(parameters);
}
private MultiMap<CompletionSorterImpl, LookupElement> groupItemsBySorter(List<LookupElement> source) {
MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = MultiMap.createLinked();
for (LookupElement element : source) {
inputBySorter.putValue(obtainSorter(element), element);
}
return inputBySorter;
}
@NotNull
private CompletionSorterImpl obtainSorter(LookupElement element) {
return myProcess.getSorter(element);
}
@Override
public Map<LookupElement, StringBuilder> getRelevanceStrings() {
final LinkedHashMap<LookupElement,StringBuilder> map = new LinkedHashMap<LookupElement, StringBuilder>();
for (LookupElement item : myItems) {
map.put(item, new StringBuilder());
}
final MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupItemsBySorter(new ArrayList<LookupElement>(map.keySet()));
if (inputBySorter.size() > 1) {
for (LookupElement element : map.keySet()) {
map.get(element).append(obtainSorter(element)).append(": ");
}
}
for (CompletionSorterImpl sorter : inputBySorter.keySet()) {
final LinkedHashMap<LookupElement, StringBuilder> subMap = new LinkedHashMap<LookupElement, StringBuilder>();
for (LookupElement element : inputBySorter.get(sorter)) {
subMap.put(element, map.get(element));
}
Classifier<LookupElement> classifier = myClassifiers.get(sorter);
if (classifier != null) {
classifier.describeItems(subMap, createContext(false));
}
}
return map;
}
@Override
public void addElement(Lookup lookup, LookupElement element, LookupElementPresentation presentation) {
StatisticsWeigher.clearBaseStatisticsInfo(element);
final String invariant = presentation.getItemText() + "\0###" + getTailTextOrSpace(presentation) + "###" + presentation.getTypeText();
element.putUserData(PRESENTATION_INVARIANT, invariant);
CompletionSorterImpl sorter = obtainSorter(element);
Classifier<LookupElement> classifier = myClassifiers.get(sorter);
if (classifier == null) {
myClassifiers.put(sorter, classifier = sorter.buildClassifier(new AlphaClassifier((LookupImpl)lookup)));
}
classifier.addElement(element, createContext(true));
super.addElement(lookup, element, presentation);
}
@NotNull
private static String getTailTextOrSpace(LookupElementPresentation presentation) {
String tailText = presentation.getTailText();
return tailText == null || tailText.isEmpty() ? " " : tailText;
}
private static List<LookupElement> sortByPresentation(Iterable<LookupElement> source, LookupImpl lookup) {
ArrayList<LookupElement> startMatches = ContainerUtil.newArrayList();
ArrayList<LookupElement> middleMatches = ContainerUtil.newArrayList();
for (LookupElement element : source) {
(CompletionServiceImpl.isStartMatch(element, lookup) ? startMatches : middleMatches).add(element);
}
ContainerUtil.sort(startMatches, BY_PRESENTATION_COMPARATOR);
ContainerUtil.sort(middleMatches, BY_PRESENTATION_COMPARATOR);
startMatches.addAll(middleMatches);
return startMatches;
}
private static boolean isAlphaSorted() {
return ourUISettings.SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
}
@Override
public Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup, boolean onExplicitAction) {
List<LookupElement> items = getMatchingItems();
MultiMap<CompletionSorterImpl, LookupElement> itemsBySorter = groupItemsBySorter(items);
LookupElement relevantSelection = findMostRelevantItem(itemsBySorter);
LookupImpl lookupImpl = (LookupImpl)lookup;
List<LookupElement> listModel = isAlphaSorted() ?
sortByPresentation(items, lookupImpl) :
fillModelByRelevance(lookupImpl, items, itemsBySorter, relevantSelection);
int toSelect = getItemToSelect(lookupImpl, listModel, onExplicitAction, relevantSelection);
LOG.assertTrue(toSelect >= 0);
addDummyItems(items.size() - listModel.size(), listModel);
return new Pair<List<LookupElement>, Integer>(listModel, toSelect);
}
private static void addDummyItems(int count, List<LookupElement> listModel) {
EmptyLookupItem dummy = new EmptyLookupItem("loading...", true);
for (int i = count; i > 0; i--) {
listModel.add(dummy);
}
}
private List<LookupElement> fillModelByRelevance(LookupImpl lookup,
List<LookupElement> items,
MultiMap<CompletionSorterImpl, LookupElement> inputBySorter,
@Nullable LookupElement relevantSelection) {
Iterator<LookupElement> byRelevance = sortByRelevance(inputBySorter).iterator();
final LinkedHashSet<LookupElement> model = new LinkedHashSet<LookupElement>();
addPrefixItems(model);
addFrozenItems(items, model);
addSomeItems(model, byRelevance, new Condition<LookupElement>() {
@Override
public boolean value(LookupElement lastAdded) {
return model.size() >= MAX_PREFERRED_COUNT;
}
});
addCurrentlySelectedItemToTop(lookup, items, model);
freezeTopItems(lookup, model);
ensureItemAdded(items, model, byRelevance, lookup.getCurrentItem());
ensureItemAdded(items, model, byRelevance, relevantSelection);
ensureEverythingVisibleAdded(lookup, model, byRelevance);
return new ArrayList<LookupElement>(model);
}
private static void ensureEverythingVisibleAdded(LookupImpl lookup, final LinkedHashSet<LookupElement> model, Iterator<LookupElement> byRelevance) {
JList list = lookup.getList();
final boolean testMode = ApplicationManager.getApplication().isUnitTestMode();
final int limit = Math.max(list.getLastVisibleIndex(), model.size()) + ourUISettings.MAX_LOOKUP_LIST_HEIGHT * 3;
addSomeItems(model, byRelevance, new Condition<LookupElement>() {
@Override
public boolean value(LookupElement lastAdded) {
return !testMode && model.size() >= limit;
}
});
}
private static void ensureItemAdded(List<LookupElement> items,
LinkedHashSet<LookupElement> model,
Iterator<LookupElement> byRelevance, @Nullable final LookupElement item) {
if (item != null && ContainerUtil.indexOfIdentity(items, item) >= 0 && !model.contains(item)) {
addSomeItems(model, byRelevance, new Condition<LookupElement>() {
@Override
public boolean value(LookupElement lastAdded) {
return lastAdded == item;
}
});
}
}
private void freezeTopItems(LookupImpl lookup, LinkedHashSet<LookupElement> model) {
myFrozenItems.clear();
if (lookup.isShown()) {
myFrozenItems.addAll(model);
}
}
private void addFrozenItems(List<LookupElement> items, LinkedHashSet<LookupElement> model) {
myFrozenItems.retainAll(items);
model.addAll(myFrozenItems);
}
private void addPrefixItems(LinkedHashSet<LookupElement> model) {
ContainerUtil.addAll(model, sortByRelevance(groupItemsBySorter(getPrefixItems(true))));
ContainerUtil.addAll(model, sortByRelevance(groupItemsBySorter(getPrefixItems(false))));
}
private static void addCurrentlySelectedItemToTop(Lookup lookup, List<LookupElement> items, LinkedHashSet<LookupElement> model) {
if (!lookup.isSelectionTouched()) {
LookupElement lastSelection = lookup.getCurrentItem();
if (ContainerUtil.indexOfIdentity(items, lastSelection) >= 0) {
model.add(lastSelection);
}
}
}
private static void addSomeItems(LinkedHashSet<LookupElement> model, Iterator<LookupElement> iterator, Condition<LookupElement> stopWhen) {
while (iterator.hasNext()) {
LookupElement item = iterator.next();
model.add(item);
if (stopWhen.value(item)) {
break;
}
}
}
private Iterable<LookupElement> sortByRelevance(MultiMap<CompletionSorterImpl, LookupElement> inputBySorter) {
final List<Iterable<LookupElement>> byClassifier = ContainerUtil.newArrayList();
for (CompletionSorterImpl sorter : myClassifiers.keySet()) {
ProcessingContext context = createContext(false);
byClassifier.add(myClassifiers.get(sorter).classify(inputBySorter.get(sorter), context));
}
//noinspection unchecked
return ContainerUtil.concat(byClassifier.toArray(new Iterable[byClassifier.size()]));
}
private ProcessingContext createContext(boolean pureRelevance) {
ProcessingContext context = new ProcessingContext();
context.put(PREFIX_CHANGES, myPrefixChanges);
context.put(WEIGHING_CONTEXT, myProcess.getLookup());
if (pureRelevance) {
context.put(PURE_RELEVANCE, Boolean.TRUE);
}
return context;
}
@Override
public LookupArranger createEmptyCopy() {
return new CompletionLookupArranger(myParameters, myProcess);
}
private static int getItemToSelect(LookupImpl lookup, List<LookupElement> items, boolean onExplicitAction, @Nullable LookupElement mostRelevant) {
if (items.isEmpty() || lookup.getFocusDegree() == LookupImpl.FocusDegree.UNFOCUSED) {
return 0;
}
if (lookup.isSelectionTouched() || !onExplicitAction) {
final LookupElement lastSelection = lookup.getCurrentItem();
int old = ContainerUtil.indexOfIdentity(items, lastSelection);
if (old >= 0) {
return old;
}
Object selectedValue = lookup.getList().getSelectedValue();
if (selectedValue instanceof EmptyLookupItem && ((EmptyLookupItem)selectedValue).isLoading()) {
int index = lookup.getList().getSelectedIndex();
if (index >= 0 && index < items.size()) {
return index;
}
}
for (int i = 0; i < items.size(); i++) {
String invariant = PRESENTATION_INVARIANT.get(items.get(i));
if (invariant != null && invariant.equals(PRESENTATION_INVARIANT.get(lastSelection))) {
return i;
}
}
}
String selectedText = lookup.getEditor().getSelectionModel().getSelectedText();
for (int i = 0; i < items.size(); i++) {
LookupElement item = items.get(i);
boolean isTemplate = isLiveTemplate(item);
if (isPrefixItem(lookup, item, true) && !isTemplate ||
item.getLookupString().equals(selectedText)) {
return i;
}
if (i == 0 && isTemplate && items.size() > 1 && !CompletionServiceImpl.isStartMatch(items.get(1), lookup)) {
return 0;
}
}
return Math.max(0, ContainerUtil.indexOfIdentity(items, mostRelevant));
}
@Nullable
private LookupElement findMostRelevantItem(MultiMap<CompletionSorterImpl, LookupElement> itemsBySorter) {
final CompletionPreselectSkipper[] skippers = CompletionPreselectSkipper.EP_NAME.getExtensions();
for (CompletionSorterImpl sorter : myClassifiers.keySet()) {
ProcessingContext context = createContext(true);
for (LookupElement element : myClassifiers.get(sorter).classify(itemsBySorter.get(sorter), context)) {
if (!shouldSkip(skippers, element)) {
return element;
}
}
}
return null;
}
private static boolean isLiveTemplate(LookupElement element) {
return element instanceof LiveTemplateLookupElement && ((LiveTemplateLookupElement)element).sudden;
}
public static StatisticsUpdate collectStatisticChanges(LookupElement item, final Lookup lookup) {
applyLastCompletionStatisticsUpdate();
final StatisticsInfo base = StatisticsWeigher.getBaseStatisticsInfo(item, null);
if (base == StatisticsInfo.EMPTY) {
return new StatisticsUpdate(StatisticsInfo.EMPTY);
}
StatisticsUpdate update = new StatisticsUpdate(StatisticsWeigher.composeStatsWithPrefix(base, lookup.itemPattern(item), true));
ourPendingUpdate = update;
Disposer.register(update, new Disposable() {
@Override
public void dispose() {
//noinspection AssignmentToStaticFieldFromInstanceMethod
ourPendingUpdate = null;
}
});
return update;
}
public static void trackStatistics(InsertionContext context, final StatisticsUpdate update) {
if (ourPendingUpdate != update) {
return;
}
final Document document = context.getDocument();
int startOffset = context.getStartOffset();
int tailOffset = context.getEditor().getCaretModel().getOffset();
if (startOffset < 0 || tailOffset <= startOffset) {
return;
}
final RangeMarker marker = document.createRangeMarker(startOffset, tailOffset);
final DocumentAdapter listener = new DocumentAdapter() {
@Override
public void beforeDocumentChange(DocumentEvent e) {
if (!marker.isValid() || e.getOffset() > marker.getStartOffset() && e.getOffset() < marker.getEndOffset()) {
cancelLastCompletionStatisticsUpdate();
}
}
};
ourStatsAlarm.addRequest(new Runnable() {
@Override
public void run() {
if (ourPendingUpdate == update) {
applyLastCompletionStatisticsUpdate();
}
}
}, 20 * 1000);
document.addDocumentListener(listener);
Disposer.register(update, new Disposable() {
@Override
public void dispose() {
document.removeDocumentListener(listener);
marker.dispose();
ourStatsAlarm.cancelAllRequests();
}
});
}
public static void cancelLastCompletionStatisticsUpdate() {
if (ourPendingUpdate != null) {
Disposer.dispose(ourPendingUpdate);
assert ourPendingUpdate == null;
}
}
public static void applyLastCompletionStatisticsUpdate() {
StatisticsUpdate update = ourPendingUpdate;
if (update != null) {
update.performUpdate();
Disposer.dispose(update);
assert ourPendingUpdate == null;
}
}
private boolean shouldSkip(CompletionPreselectSkipper[] skippers, LookupElement element) {
for (final CompletionPreselectSkipper skipper : skippers) {
if (skipper.skipElement(element, myLocation)) {
if (LOG.isDebugEnabled()) {
LOG.debug("Skipped element " + element + " by " + skipper);
}
return true;
}
}
return false;
}
@Override
public void prefixChanged(Lookup lookup) {
myPrefixChanges++;
myFrozenItems.clear();
super.prefixChanged(lookup);
}
static class StatisticsUpdate implements Disposable {
private final StatisticsInfo myInfo;
private int mySpared;
public StatisticsUpdate(StatisticsInfo info) {
myInfo = info;
}
void performUpdate() {
myInfo.incUseCount();
((FeatureUsageTrackerImpl)FeatureUsageTracker.getInstance()).getCompletionStatistics().registerInvocation(mySpared);
}
@Override
public void dispose() {
}
public void addSparedChars(CompletionProgressIndicator indicator, LookupElement item, InsertionContext context, char completionChar) {
String textInserted;
if (context.getStartOffset() >= 0 && context.getTailOffset() >= context.getStartOffset()) {
textInserted = context.getDocument().getImmutableCharSequence().subSequence(context.getStartOffset(), context.getTailOffset()).toString();
} else {
textInserted = item.getLookupString();
}
String withoutSpaces = StringUtil.replace(textInserted, new String[]{" ", "\t", "\n"}, new String[]{"", "", ""});
int spared = withoutSpaces.length() - indicator.getLookup().itemPattern(item).length();
if (!LookupEvent.isSpecialCompletionChar(completionChar) && withoutSpaces.contains(String.valueOf(completionChar))) {
spared--;
}
if (spared > 0) {
mySpared += spared;
}
}
}
private static class AlphaClassifier extends Classifier<LookupElement> {
private final LookupImpl myLookup;
private AlphaClassifier(LookupImpl lookup) {
myLookup = lookup;
}
@Override
public void addElement(LookupElement element, ProcessingContext context) {
}
@Override
public Iterable<LookupElement> classify(Iterable<LookupElement> source, ProcessingContext context) {
return sortByPresentation(source, myLookup);
}
@Override
public void describeItems(LinkedHashMap<LookupElement, StringBuilder> map, ProcessingContext context) {
}
}
}