/* * Copyright 2000-2017 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.ide.ui.UISettings; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.util.*; import com.intellij.openapi.util.registry.Registry; import com.intellij.patterns.StandardPatterns; import com.intellij.util.ProcessingContext; import com.intellij.util.SmartList; import com.intellij.util.containers.ContainerUtil; import com.intellij.util.containers.MultiMap; import com.intellij.util.containers.hash.EqualityPolicy; import gnu.trove.THashSet; 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"); private static final Key<PresentationInvariant> GLOBAL_PRESENTATION_INVARIANT = Key.create("PRESENTATION_INVARIANT"); private final Key<PresentationInvariant> PRESENTATION_INVARIANT = Key.create("PRESENTATION_INVARIANT"); private final Comparator<LookupElement> BY_PRESENTATION_COMPARATOR = (o1, o2) -> { PresentationInvariant invariant = PRESENTATION_INVARIANT.get(o1); assert invariant != null; return invariant.compareTo(PRESENTATION_INVARIANT.get(o2)); }; static final int MAX_PREFERRED_COUNT = 5; public static final String OVERFLOW_MESSAGE = "Not all variants are shown, please type more letters to see the rest"; public static final Key<WeighingContext> WEIGHING_CONTEXT = Key.create("WEIGHING_CONTEXT"); 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<>(); private final int myLimit = Registry.intValue("ide.completion.variant.limit"); private boolean myOverflow; private final CompletionLocation myLocation; private final CompletionParameters myParameters; private final CompletionProgressIndicator myProcess; @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"}) private final Map<CompletionSorterImpl, Classifier<LookupElement>> myClassifiers = new LinkedHashMap<>(); private final Key<CompletionSorterImpl> mySorterKey = Key.create("SORTER_KEY"); private final CompletionFinalSorter myFinalSorter = CompletionFinalSorter.newSorter(); private int myPrefixChanges; public CompletionLookupArranger(final CompletionParameters parameters, CompletionProgressIndicator process) { myParameters = parameters; myProcess = process; myLocation = new CompletionLocation(parameters); } private MultiMap<CompletionSorterImpl, LookupElement> groupItemsBySorter(Iterable<LookupElement> source) { MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = MultiMap.createLinked(); for (LookupElement element : source) { inputBySorter.putValue(obtainSorter(element), element); } for (CompletionSorterImpl sorter : inputBySorter.keySet()) { inputBySorter.put(sorter, sortByPresentation(inputBySorter.get(sorter))); } return inputBySorter; } @NotNull private CompletionSorterImpl obtainSorter(LookupElement element) { //noinspection ConstantConditions return element.getUserData(mySorterKey); } @NotNull @Override public Map<LookupElement, List<Pair<String, Object>>> getRelevanceObjects(@NotNull Iterable<LookupElement> items, boolean hideSingleValued) { Map<LookupElement, List<Pair<String, Object>>> map = ContainerUtil.newIdentityHashMap(); MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupItemsBySorter(items); int sorterNumber = 0; for (CompletionSorterImpl sorter : inputBySorter.keySet()) { sorterNumber++; Collection<LookupElement> thisSorterItems = inputBySorter.get(sorter); for (LookupElement element : thisSorterItems) { map.put(element, ContainerUtil.newArrayList(new Pair<>("frozen", myFrozenItems.contains(element)), new Pair<>("sorter", sorterNumber))); } ProcessingContext context = createContext(); Classifier<LookupElement> classifier = myClassifiers.get(sorter); while (classifier != null) { final THashSet<LookupElement> itemSet = ContainerUtil.newIdentityTroveSet(thisSorterItems); List<LookupElement> unsortedItems = ContainerUtil.filter(myItems, lookupElement -> itemSet.contains(lookupElement)); List<Pair<LookupElement, Object>> pairs = classifier.getSortingWeights(unsortedItems, context); if (!hideSingleValued || !haveSameWeights(pairs)) { for (Pair<LookupElement, Object> pair : pairs) { map.get(pair.first).add(Pair.create(classifier.getPresentableName(), pair.second)); } } classifier = classifier.getNext(); } } //noinspection unchecked Map<LookupElement, List<Pair<String, Object>>> result = new com.intellij.util.containers.hash.LinkedHashMap(EqualityPolicy.IDENTITY); Map<LookupElement, List<Pair<String, Object>>> additional = myFinalSorter.getRelevanceObjects(items); for (LookupElement item : items) { List<Pair<String, Object>> mainRelevance = map.get(item); List<Pair<String, Object>> additionalRelevance = additional.get(item); result.put(item, additionalRelevance == null ? mainRelevance : ContainerUtil.concat(mainRelevance, additionalRelevance)); } return result; } void associateSorter(LookupElement element, CompletionSorterImpl sorter) { element.putUserData(mySorterKey, sorter); } private static boolean haveSameWeights(List<Pair<LookupElement, Object>> pairs) { if (pairs.isEmpty()) return true; for (int i = 1; i < pairs.size(); i++) { if (!Comparing.equal(pairs.get(i).second, pairs.get(0).second)) { return false; } } return true; } @Override public void addElement(LookupElement element, LookupElementPresentation presentation) { StatisticsWeigher.clearBaseStatisticsInfo(element); PresentationInvariant invariant = new PresentationInvariant(presentation.getItemText(), presentation.getTailText(), presentation.getTypeText()); element.putUserData(PRESENTATION_INVARIANT, invariant); element.putUserData(GLOBAL_PRESENTATION_INVARIANT, invariant); CompletionSorterImpl sorter = obtainSorter(element); Classifier<LookupElement> classifier = myClassifiers.get(sorter); if (classifier == null) { myClassifiers.put(sorter, classifier = sorter.buildClassifier(new EmptyClassifier())); } ProcessingContext context = createContext(); classifier.addElement(element, context); super.addElement(element, presentation); trimToLimit(context); } @Override public void itemSelected(@Nullable LookupElement lookupItem, char completionChar) { myProcess.itemSelected(lookupItem, completionChar); } private void trimToLimit(ProcessingContext context) { if (myItems.size() < myLimit) return; List<LookupElement> items = getMatchingItems(); Iterator<LookupElement> iterator = sortByRelevance(groupItemsBySorter(items)).iterator(); final Set<LookupElement> retainedSet = ContainerUtil.newIdentityTroveSet(); retainedSet.addAll(getPrefixItems(true)); retainedSet.addAll(getPrefixItems(false)); retainedSet.addAll(myFrozenItems); while (retainedSet.size() < myLimit / 2 && iterator.hasNext()) { retainedSet.add(iterator.next()); } if (!iterator.hasNext()) return; List<LookupElement> removed = retainItems(retainedSet); for (LookupElement element : removed) { removeItem(element, context); } if (!myOverflow) { myOverflow = true; myProcess.addAdvertisement(OVERFLOW_MESSAGE, null); // restart completion on any prefix change myProcess.addWatchedPrefix(0, StandardPatterns.string()); } } private void removeItem(LookupElement element, ProcessingContext context) { CompletionSorterImpl sorter = obtainSorter(element); Classifier<LookupElement> classifier = myClassifiers.get(sorter); classifier.removeElement(element, context); } private List<LookupElement> sortByPresentation(Iterable<LookupElement> source) { ArrayList<LookupElement> startMatches = ContainerUtil.newArrayList(); ArrayList<LookupElement> middleMatches = ContainerUtil.newArrayList(); for (LookupElement element : source) { (itemMatcher(element).isStartMatch(element) ? 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.getSortLookupElementsLexicographically(); } @Override public Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup, boolean onExplicitAction) { List<LookupElement> items = getMatchingItems(); Iterable<LookupElement> sortedByRelevance = sortByRelevance(groupItemsBySorter(items)); LookupElement relevantSelection = findMostRelevantItem(sortedByRelevance); LookupImpl lookupImpl = (LookupImpl)lookup; List<LookupElement> listModel = isAlphaSorted() ? sortByPresentation(items) : fillModelByRelevance(lookupImpl, ContainerUtil.newIdentityTroveSet(items), sortedByRelevance, relevantSelection); int toSelect = getItemToSelect(lookupImpl, listModel, onExplicitAction, relevantSelection); LOG.assertTrue(toSelect >= 0); addDummyItems(items.size() - listModel.size(), listModel); return new Pair<>(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, Set<LookupElement> items, Iterable<LookupElement> sortedElements, @Nullable LookupElement relevantSelection) { Iterator<LookupElement> byRelevance = sortedElements.iterator(); final LinkedHashSet<LookupElement> model = new LinkedHashSet<>(); addPrefixItems(model); addFrozenItems(items, model); if (model.size() < MAX_PREFERRED_COUNT) { addSomeItems(model, byRelevance, lastAdded -> 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<>(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.getMaxLookupListHeight() * 3; addSomeItems(model, byRelevance, lastAdded -> !testMode && model.size() >= limit); } private static void ensureItemAdded(Set<LookupElement> items, LinkedHashSet<LookupElement> model, Iterator<LookupElement> byRelevance, @Nullable final LookupElement item) { if (item != null && items.contains(item) && !model.contains(item)) { addSomeItems(model, byRelevance, lastAdded -> lastAdded == item); } } private void freezeTopItems(LookupImpl lookup, LinkedHashSet<LookupElement> model) { myFrozenItems.clear(); if (lookup.isShown()) { myFrozenItems.addAll(model); } } private void addFrozenItems(Set<LookupElement> items, LinkedHashSet<LookupElement> model) { for (Iterator<LookupElement> iterator = myFrozenItems.iterator(); iterator.hasNext(); ) { LookupElement element = iterator.next(); if (!element.isValid() || !items.contains(element)) { iterator.remove(); } } 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, Set<LookupElement> items, LinkedHashSet<LookupElement> model) { if (!lookup.isSelectionTouched()) { LookupElement lastSelection = lookup.getCurrentItem(); if (items.contains(lastSelection)) { 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(); byClassifier.add(myClassifiers.get(sorter).classify(inputBySorter.get(sorter), context)); } //noinspection unchecked Iterable<LookupElement> result = ContainerUtil.concat(byClassifier.toArray(new Iterable[byClassifier.size()])); return myFinalSorter.sort(result, myParameters); } private ProcessingContext createContext() { ProcessingContext context = new ProcessingContext(); context.put(PREFIX_CHANGES, myPrefixChanges); context.put(WEIGHING_CONTEXT, this); return context; } @Override public LookupArranger createEmptyCopy() { return new CompletionLookupArranger(myParameters, myProcess); } private 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++) { PresentationInvariant invariant = PRESENTATION_INVARIANT.get(items.get(i)); if (invariant != null && invariant.equals(GLOBAL_PRESENTATION_INVARIANT.get(lastSelection))) { return i; } } } LookupElement exactMatch = getBestExactMatch(lookup, items); return Math.max(0, ContainerUtil.indexOfIdentity(items, exactMatch != null ? exactMatch : mostRelevant)); } private List<LookupElement> getExactMatches(LookupImpl lookup, List<LookupElement> items) { String selectedText = lookup.getTopLevelEditor().getSelectionModel().getSelectedText(); List<LookupElement> exactMatches = new SmartList<>(); for (int i = 0; i < items.size(); i++) { LookupElement item = items.get(i); boolean isSuddenLiveTemplate = isSuddenLiveTemplate(item); if (isPrefixItem(item, true) && !isSuddenLiveTemplate || item.getLookupString().equals(selectedText)) { if (item instanceof LiveTemplateLookupElement) { // prefer most recent live template lookup item return Collections.singletonList(item); } exactMatches.add(item); } else if (i == 0 && isSuddenLiveTemplate && items.size() > 1 && !CompletionServiceImpl.isStartMatch(items.get(1), this)) { return Collections.singletonList(item); } } return exactMatches; } @Nullable private LookupElement getBestExactMatch(LookupImpl lookup, List<LookupElement> items) { List<LookupElement> exactMatches = getExactMatches(lookup, items); if (exactMatches.isEmpty()) return null; if (exactMatches.size() == 1) { return exactMatches.get(0); } return sortByRelevance(groupItemsBySorter(exactMatches)).iterator().next(); } @Nullable private LookupElement findMostRelevantItem(Iterable<LookupElement> sorted) { final CompletionPreselectSkipper[] skippers = CompletionPreselectSkipper.EP_NAME.getExtensions(); for (LookupElement element : sorted) { if (!shouldSkip(skippers, element)) { return element; } } return null; } private static boolean isSuddenLiveTemplate(LookupElement element) { return element instanceof LiveTemplateLookupElement && ((LiveTemplateLookupElement)element).sudden; } 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); } private static class EmptyClassifier extends Classifier<LookupElement> { private EmptyClassifier() { super(null, "empty"); } @NotNull @Override public List<Pair<LookupElement, Object>> getSortingWeights(@NotNull Iterable<LookupElement> items, @NotNull ProcessingContext context) { return Collections.emptyList(); } @NotNull @Override public Iterable<LookupElement> classify(@NotNull Iterable<LookupElement> source, @NotNull ProcessingContext context) { return source; } } }