more control over lookup item order and selection

This commit is contained in:
peter
2012-04-24 12:53:56 +02:00
parent 0b17a54868
commit 3fc80399cd
9 changed files with 417 additions and 545 deletions
@@ -63,10 +63,12 @@ public interface Lookup {
boolean isFocused();
@NotNull
String itemPattern(LookupElement element);
String itemPattern(@NotNull LookupElement element);
@NotNull
PrefixMatcher itemMatcher(LookupElement item);
PrefixMatcher itemMatcher(@NotNull LookupElement item);
boolean isSelectionTouched();
boolean prefixMatches(@NotNull LookupElement item);
}
@@ -253,7 +253,8 @@ public class CodeCompletionHandlerBase implements CodeInsightActionHandler {
return existing;
}
LookupImpl lookup = (LookupImpl)LookupManager.getInstance(editor.getProject()).createLookup(editor, LookupElement.EMPTY_ARRAY, "", LookupArranger.DEFAULT);
LookupImpl lookup = (LookupImpl)LookupManager.getInstance(editor.getProject()).createLookup(editor, LookupElement.EMPTY_ARRAY, "",
new LookupArranger.DefaultArranger());
if (editor.isOneLineMode()) {
lookup.setCancelOnClickOutside(true);
lookup.setCancelOnOtherWindowOpen(true);
@@ -18,10 +18,10 @@ package com.intellij.codeInsight.completion;
import com.google.common.collect.Maps;
import com.intellij.codeInsight.completion.impl.CompletionSorterImpl;
import com.intellij.codeInsight.lookup.Classifier;
import com.intellij.codeInsight.lookup.LookupArranger;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.*;
import com.intellij.codeInsight.lookup.impl.LookupImpl;
import com.intellij.codeInsight.template.impl.LiveTemplateLookupElement;
import com.intellij.ide.ui.UISettings;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.editor.Document;
@@ -29,15 +29,17 @@ 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.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.WeighingService;
import com.intellij.psi.statistics.StatisticsInfo;
import com.intellij.psi.statistics.StatisticsManager;
import com.intellij.util.Alarm;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FactoryMap;
import com.intellij.util.containers.MultiMap;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -47,7 +49,9 @@ import java.util.*;
public class CompletionLookupArranger extends LookupArranger {
@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 final List<LookupElement> myFrozenItems = new ArrayList<LookupElement>();
static {
Disposer.register(ApplicationManager.getApplication(), new Disposable() {
@Override
@@ -60,25 +64,229 @@ public class CompletionLookupArranger extends LookupArranger {
private static final String SELECTED = "selected";
static final String IGNORED = "ignored";
private final CompletionLocation myLocation;
private final Map<LookupElement, Comparable> mySortingWeights = new THashMap<LookupElement, Comparable>(TObjectHashingStrategy.IDENTITY);
@SuppressWarnings("unchecked") private final Map<LookupElement, Comparable> mySortingWeights = new THashMap<LookupElement, Comparable>(TObjectHashingStrategy.IDENTITY);
private final CompletionParameters myParameters;
private final CompletionProgressIndicator myProcess;
private final Classifier<LookupElement> myClassifier;
public CompletionLookupArranger(final CompletionParameters parameters, CompletionProgressIndicator process) {
myParameters = parameters;
myProcess = process;
myLocation = new CompletionLocation(parameters);
myClassifier = new Classifier<LookupElement>() {
@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
private final FactoryMap<CompletionSorterImpl, Classifier<LookupElement>> myClassifiers =
new FactoryMap<CompletionSorterImpl, Classifier<LookupElement>>() {
@Override
protected Map<CompletionSorterImpl, Classifier<LookupElement>> createMap() {
return Maps.newLinkedHashMap();
}
@Override
protected Classifier<LookupElement> create(CompletionSorterImpl key) {
return key.buildClassifier();
}
};
@Override
public void addElement(LookupElement element) {
mySortingWeights.put(element, WeighingService.weigh(CompletionService.SORTING_KEY, element, myLocation));
myClassifiers.get(obtainSorter(element)).addElement(element);
}
@Override
public Iterable<List<LookupElement>> classify(List<LookupElement> source) {
MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(source);
final ArrayList<List<LookupElement>> result = new ArrayList<List<LookupElement>>();
for (CompletionSorterImpl sorter : myClassifiers.keySet()) {
ContainerUtil.addAll(result, myClassifiers.get(sorter).classify((List<LookupElement>)inputBySorter.get(sorter)));
}
return result;
}
private MultiMap<CompletionSorterImpl, LookupElement> groupInputBySorter(List<LookupElement> source) {
MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = new MultiMap<CompletionSorterImpl, LookupElement>();
for (LookupElement element : source) {
inputBySorter.putValue(obtainSorter(element), element);
}
return inputBySorter;
}
@NotNull
private CompletionSorterImpl obtainSorter(LookupElement element) {
return myProcess.getSorter(element);
}
@Override
public void describeItems(LinkedHashMap<LookupElement, StringBuilder> map) {
final MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(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));
}
myClassifiers.get(sorter).describeItems(subMap);
}
}
};
}
@Override
@NotNull
public Comparator<LookupElement> getItemComparator() {
return new Comparator<LookupElement>() {
public Map<LookupElement, StringBuilder> getRelevanceStrings() {
final LinkedHashMap<LookupElement,StringBuilder> map = new LinkedHashMap<LookupElement, StringBuilder>();
for (LookupElement item : myItems) {
map.put(item, new StringBuilder());
}
myClassifier.describeItems(map);
return map;
}
@Override
public void addElement(Lookup lookup, LookupElement item, LookupElementPresentation presentation) {
myClassifier.addElement(item);
final String invariant = presentation.getItemText() + "###" + presentation.getTailText() + "###" + presentation.getTypeText();
item.putUserData(PRESENTATION_INVARIANT, invariant);
super.addElement(lookup, item, presentation);
}
private static boolean isAlphaSorted() {
return UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
}
@Override
public Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup) {
List<LookupElement> items = matchingItems(lookup);
Collections.sort(items, new Comparator<LookupElement>() {
public int compare(LookupElement o1, LookupElement o2) {
//noinspection unchecked
return mySortingWeights.get(o1).compareTo(mySortingWeights.get(o2));
}
};
});
Iterable<List<LookupElement>> groups = myClassifier.classify(items);
LinkedHashSet<LookupElement> model = new LinkedHashSet<LookupElement>();
for (List<LookupElement> group : groups) {
addPrefixItems(lookup, model, true, group);
}
for (List<LookupElement> group : groups) {
addPrefixItems(lookup, model, false, group);
}
myFrozenItems.retainAll(items);
model.addAll(myFrozenItems);
if (!isAlphaSorted()) {
addMostRelevantItems(model, groups);
LookupElement lastSelection = lookup.getCurrentItem();
if (items.contains(lastSelection)) {
model.add(lastSelection);
}
}
myFrozenItems.clear();
if (((LookupImpl)lookup).isShown()) {
myFrozenItems.addAll(model);
}
if (isAlphaSorted()) {
Collections.sort(items, new Comparator<LookupElement>() {
@Override
public int compare(LookupElement o1, LookupElement o2) {
return o1.getLookupString().compareToIgnoreCase(o2.getLookupString());
}
});
model.addAll(items);
} else {
for (List<LookupElement> group : groups) {
model.addAll(group);
}
}
ArrayList<LookupElement> listModel = new ArrayList<LookupElement>(model);
return new Pair<List<LookupElement>, Integer>(listModel, getItemToSelect(lookup, groups, listModel));
}
@Override
public LookupArranger createEmptyCopy() {
return new CompletionLookupArranger(myParameters, myProcess);
}
private int getItemToSelect(Lookup lookup, Iterable<List<LookupElement>> groups, List<LookupElement> items) {
if (items.isEmpty() || !lookup.isFocused()) {
return 0;
}
if (!lookup.isSelectionTouched()) {
LookupElement first = items.get(0);
if (isExactPrefixItem(lookup, first, true) && !isLiveTemplate(first)) {
return 0;
}
}
LookupElement lastSelection = lookup.getCurrentItem();
int old = items.indexOf(lastSelection);
if (old >= 0) {
return old;
}
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;
}
}
for (int i = 0; i < items.size(); i++) {
LookupElement item = items.get(i);
if (isExactPrefixItem(lookup, item, true) && !isLiveTemplate(item)) {
return i;
}
}
final CompletionPreselectSkipper[] skippers = CompletionPreselectSkipper.EP_NAME.getExtensions();
for (List<LookupElement> group : groups) {
for (LookupElement element : group) {
if (!shouldSkip(skippers, element)) {
return items.indexOf(element);
}
}
}
return items.size() - 1;
}
private static boolean isLiveTemplate(LookupElement element) {
return element instanceof LiveTemplateLookupElement && ((LiveTemplateLookupElement)element).sudden;
}
private static final int MAX_PREFERRED_COUNT = 5;
private static void addMostRelevantItems(final Set<LookupElement> model, final Iterable<List<LookupElement>> sortedItems) {
if (model.size() > MAX_PREFERRED_COUNT) return;
for (final List<LookupElement> elements : sortedItems) {
final List<LookupElement> suitable = new SmartList<LookupElement>();
for (final LookupElement item : elements) {
if (!model.contains(item)) {
suitable.add(item);
}
}
if (model.size() + suitable.size() > MAX_PREFERRED_COUNT) break;
model.addAll(suitable);
}
}
public static StatisticsUpdate collectStatisticChanges(CompletionProgressIndicator indicator, LookupElement item) {
LookupImpl lookupImpl = indicator.getLookup();
applyLastCompletionStatisticsUpdate();
@@ -172,23 +380,6 @@ public class CompletionLookupArranger extends LookupArranger {
}
}
public int suggestPreselectedItem(List<LookupElement> sorted, Iterable<List<LookupElement>> groups) {
final CompletionPreselectSkipper[] skippers = CompletionPreselectSkipper.EP_NAME.getExtensions();
Set<LookupElement> model = new THashSet<LookupElement>(sorted);
for (List<LookupElement> group : groups) {
for (LookupElement element : group) {
if (model.contains(element)) {
if (!shouldSkip(skippers, element)) {
return sorted.indexOf(element);
}
}
}
}
return sorted.size() - 1;
}
private boolean shouldSkip(CompletionPreselectSkipper[] skippers, LookupElement element) {
for (final CompletionPreselectSkipper skipper : skippers) {
if (skipper.skipElement(element, myLocation)) {
@@ -202,70 +393,10 @@ public class CompletionLookupArranger extends LookupArranger {
return info.getContext() + "###" + info.getValue();
}
public Classifier<LookupElement> createRelevanceClassifier() {
return new Classifier<LookupElement>() {
@SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
private final FactoryMap<CompletionSorterImpl, Classifier<LookupElement>> myClassifiers = new FactoryMap<CompletionSorterImpl, Classifier<LookupElement>>() {
@Override
protected Map<CompletionSorterImpl, Classifier<LookupElement>> createMap() {
return Maps.newLinkedHashMap();
}
@Override
protected Classifier<LookupElement> create(CompletionSorterImpl key) {
return key.buildClassifier();
}
};
@Override
public void addElement(LookupElement element) {
mySortingWeights.put(element, WeighingService.weigh(CompletionService.SORTING_KEY, element, myLocation));
myClassifiers.get(obtainSorter(element)).addElement(element);
}
@Override
public Iterable<List<LookupElement>> classify(List<LookupElement> source) {
MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(source);
final ArrayList<List<LookupElement>> result = new ArrayList<List<LookupElement>>();
for (CompletionSorterImpl sorter : myClassifiers.keySet()) {
ContainerUtil.addAll(result, myClassifiers.get(sorter).classify((List<LookupElement>)inputBySorter.get(sorter)));
}
return result;
}
private MultiMap<CompletionSorterImpl, LookupElement> groupInputBySorter(List<LookupElement> source) {
MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = new MultiMap<CompletionSorterImpl, LookupElement>();
for (LookupElement element : source) {
inputBySorter.putValue(obtainSorter(element), element);
}
return inputBySorter;
}
@NotNull
private CompletionSorterImpl obtainSorter(LookupElement element) {
return myProcess.getSorter(element);
}
@Override
public void describeItems(LinkedHashMap<LookupElement, StringBuilder> map) {
final MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(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));
}
myClassifiers.get(sorter).describeItems(subMap);
}
}
};
@Override
public void prefixChanged() {
myFrozenItems.clear();
super.prefixChanged();
}
static class StatisticsUpdate implements Disposable {
@@ -16,31 +16,89 @@
package com.intellij.codeInsight.lookup;
import org.jetbrains.annotations.Nullable;
import com.intellij.openapi.util.Pair;
import org.jetbrains.annotations.NotNull;
import java.util.Comparator;
import java.util.List;
import java.util.*;
/**
* @author peter
*/
public abstract class LookupArranger {
public static final LookupArranger DEFAULT = new LookupArranger() {
protected final List<LookupElement> myItems = new ArrayList<LookupElement>();
public void addElement(Lookup lookup, LookupElement item, LookupElementPresentation presentation) {
myItems.add(item);
}
public void prefixChanged() {
}
public abstract Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup);
public final void clearItems() {
prefixChanged();
myItems.clear();
}
public abstract LookupArranger createEmptyCopy();
protected static void addPrefixItems(Lookup lookup, LinkedHashSet<LookupElement> result, boolean caseSensitive, Collection<LookupElement> items) {
for (LookupElement element : items) {
if (isExactPrefixItem(lookup, element, caseSensitive)) {
result.add(element);
}
}
}
protected static boolean isExactPrefixItem(Lookup lookup, LookupElement item, final boolean caseSensitive) {
final String pattern = lookup.itemPattern(item);
final Set<String> strings = item.getAllLookupStrings();
if (strings.contains(pattern)) {
return caseSensitive; //to not add the same elements twice to the model, as sensitive and then as insensitive
}
if (caseSensitive) {
return false;
}
for (String s : strings) {
if (s.equalsIgnoreCase(pattern)) {
return true;
}
}
return false;
}
protected List<LookupElement> matchingItems(Lookup lookup) {
final List<LookupElement> items = new ArrayList<LookupElement>();
for (LookupElement element : myItems) {
if (lookup.prefixMatches(element)) {
items.add(element);
}
}
return items;
}
public Map<LookupElement,StringBuilder> getRelevanceStrings() {
return Collections.emptyMap();
}
public static class DefaultArranger extends LookupArranger {
public Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup) {
LinkedHashSet<LookupElement> result = new LinkedHashSet<LookupElement>();
List<LookupElement> items = matchingItems(lookup);
addPrefixItems(lookup, result, true, items);
addPrefixItems(lookup, result, false, items);
result.addAll(items);
ArrayList<LookupElement> list = new ArrayList<LookupElement>(result);
int selected = list.indexOf(lookup.getCurrentItem());
return new Pair<List<LookupElement>, Integer>(list, selected >= 0 ? selected : 0);
}
@Override
public Classifier<LookupElement> createRelevanceClassifier() {
return ClassifierFactory.listClassifier();
public LookupArranger createEmptyCopy() {
return new DefaultArranger();
}
};
public int suggestPreselectedItem(List<LookupElement> sorted, Iterable<List<LookupElement>> groups) {
return 0;
}
public abstract Classifier<LookupElement> createRelevanceClassifier();
@Nullable
public Comparator<LookupElement> getItemComparator() {
return null; //don't sort
}
}
@@ -46,12 +46,12 @@ public abstract class LookupManager {
@Nullable
public LookupEx showLookup(Editor editor, @NotNull LookupElement... items) {
return showLookup(editor, items, "", LookupArranger.DEFAULT);
return showLookup(editor, items, "", new LookupArranger.DefaultArranger());
}
@Nullable
public LookupEx showLookup(Editor editor, @NotNull LookupElement[] items, @NotNull String prefix) {
return showLookup(editor, items, prefix, LookupArranger.DEFAULT);
return showLookup(editor, items, prefix, new LookupArranger.DefaultArranger());
}
@Nullable
@@ -25,13 +25,13 @@ import com.intellij.codeInsight.completion.impl.CamelHumpMatcher;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.lookup.*;
import com.intellij.codeInsight.template.impl.LiveTemplateLookupElement;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.ide.IdeEventQueue;
import com.intellij.ide.ui.UISettings;
import com.intellij.lang.LangBundle;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.IdeActions;
import com.intellij.openapi.application.AccessToken;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.WriteAction;
@@ -40,7 +40,6 @@ import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.keymap.KeymapUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
@@ -54,7 +53,9 @@ import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.ui.*;
import com.intellij.ui.LightweightHint;
import com.intellij.ui.ListScrollingUtil;
import com.intellij.ui.ScreenUtil;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBList;
import com.intellij.ui.components.JBScrollPane;
@@ -63,7 +64,6 @@ import com.intellij.ui.popup.AbstractPopup;
import com.intellij.util.Alarm;
import com.intellij.util.CollectConsumer;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ConcurrentHashMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.AbstractLayoutManager;
@@ -88,9 +88,7 @@ import java.util.List;
public class LookupImpl extends LightweightHint implements LookupEx, Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupImpl");
private static final int MAX_PREFERRED_COUNT = 5;
private static final LookupItem EMPTY_LOOKUP_ITEM = LookupItem.fromString("preselect");
private static final Icon relevanceSortIcon = IconLoader.getIcon("/ide/lookupRelevance.png");
private static final Icon lexiSortIcon = IconLoader.getIcon("/ide/lookupAlphanumeric.png");
@@ -127,8 +125,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
private boolean myShown = false;
private boolean myDisposed = false;
private boolean myHidden = false;
private final List<LookupElement> myFrozenItems = new ArrayList<LookupElement>();
private String mySelectionInvariant = null;
private boolean mySelectionTouched;
private boolean myFocused = true;
private String myAdditionalPrefix = "";
@@ -139,8 +135,8 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
private volatile String myAdText;
private volatile int myLookupTextWidth = 50;
private boolean myChangeGuard;
private volatile LookupModel myModel = new LookupModel(EMPTY_LOOKUP_ITEM);
private LookupModel myPresentableModel = myModel;
private volatile LookupArranger myArranger;
private LookupArranger myPresentableArranger;
@SuppressWarnings("unchecked") private final Map<LookupElement, PrefixMatcher> myMatchers = new ConcurrentHashMap<LookupElement, PrefixMatcher>(TObjectHashingStrategy.IDENTITY);
private LookupHint myElementHint = null;
private Alarm myHintAlarm = new Alarm();
@@ -153,7 +149,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
private int myMaximumHeight = Integer.MAX_VALUE;
private boolean myFinishing;
public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger){
public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger) {
super(new JPanel(new BorderLayout()));
setForceShowAsPopup(true);
setCancelOnClickOutside(false);
@@ -162,6 +158,8 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
myProject = project;
myEditor = editor;
myArranger = arranger;
myPresentableArranger = arranger;
myIconPanel.setVisible(false);
myCellRenderer = new LookupCellRenderer(this);
@@ -211,7 +209,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
addEmptyItem((DefaultListModel)model);
updateListHeight(model);
setArranger(arranger);
addListeners();
@@ -257,7 +254,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
}
public void setArranger(LookupArranger arranger) {
myModel.setArranger(arranger);
myArranger = arranger;
}
@Override
@@ -299,7 +296,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
ApplicationManager.getApplication().assertIsDispatchThread();
}
mySelectionTouched = true;
myPresentableModel.preselectedItem = null;
myList.repaint();
}
@@ -309,15 +305,13 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
}
public void resort() {
myFrozenItems.clear();
myPresentableModel.preselectedItem = EMPTY_LOOKUP_ITEM;
final List<LookupElement> items = getItems();
synchronized (myList) {
myPresentableArranger.prefixChanged();
((DefaultListModel)myList.getModel()).clear();
}
final List<LookupElement> items = myPresentableModel.getItems();
myPresentableModel.clearItems();
for (final LookupElement item : items) {
addItem(item, itemMatcher(item));
}
@@ -326,17 +320,17 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
public void addItem(LookupElement item, PrefixMatcher matcher) {
myMatchers.put(item, matcher);
myModel.addItem(item);
updateLookupWidth(item);
LookupElementPresentation presentation = updateLookupWidth(item);
synchronized (myList) {
myArranger.addElement(this, item, presentation);
}
}
public void updateLookupWidth(LookupElement item) {
public LookupElementPresentation updateLookupWidth(LookupElement item) {
final LookupElementPresentation presentation = renderItemApproximately(item);
int maxWidth = myCellRenderer.updateMaximumWidth(presentation);
myLookupTextWidth = Math.max(maxWidth, myLookupTextWidth);
myModel.setItemPresentation(item, presentation);
return presentation;
}
public void requestResize() {
@@ -390,7 +384,9 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
checkValid();
myAdditionalPrefix += c;
myInitialPrefix = null;
myFrozenItems.clear();
synchronized (myList) {
myPresentableArranger.prefixChanged();
}
requestResize();
refreshUi(false);
ensureSelectionVisible();
@@ -420,9 +416,13 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
myAdditionalPrefix = myAdditionalPrefix.substring(0, len - 1);
myInitialPrefix = null;
myFrozenItems.clear();
boolean shouldUpdate;
synchronized (myList) {
shouldUpdate = myPresentableArranger == myArranger;
myPresentableArranger.prefixChanged();
}
requestResize();
if (myPresentableModel == myModel) {
if (shouldUpdate) {
refreshUi(false);
ensureSelectionVisible();
}
@@ -436,108 +436,49 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
}
checkValid();
final Trinity<List<LookupElement>, Iterable<List<LookupElement>>, Boolean> snapshot = myPresentableModel.getModelSnapshot();
final LinkedHashSet<LookupElement> items = matchingItems(snapshot.first);
checkMinPrefixLengthChanges(items);
boolean hasPreselected = !mySelectionTouched && items.contains(myPresentableModel.preselectedItem);
LookupElement oldSelected = mySelectionTouched ? (LookupElement)myList.getSelectedValue() : null;
String oldInvariant = mySelectionInvariant;
LinkedHashSet<LookupElement> model = new LinkedHashSet<LookupElement>();
model.addAll(getPrefixItems(items, true));
model.addAll(getPrefixItems(items, false));
myFrozenItems.retainAll(items);
model.addAll(myFrozenItems);
if (!isAlphaSorted()) {
addMostRelevantItems(model, snapshot.second);
if (hasPreselected) {
model.add(myPresentableModel.preselectedItem);
}
}
myFrozenItems.clear();
if (myShown) {
myFrozenItems.addAll(model);
}
if (isAlphaSorted()) {
final ArrayList<LookupElement> elements = new ArrayList<LookupElement>(items);
Collections.sort(elements, new Comparator<LookupElement>() {
@Override
public int compare(LookupElement o1, LookupElement o2) {
return o1.getLookupString().compareToIgnoreCase(o2.getLookupString());
}
});
model.addAll(elements);
} else {
for (List<LookupElement> group : snapshot.second) {
for (LookupElement element : group) {
if (prefixMatches(element)) {
model.add(element);
}
}
}
}
DefaultListModel listModel = (DefaultListModel)myList.getModel();
synchronized (myList) {
listModel.clear();
Pair<List<LookupElement>, Integer> pair = myPresentableArranger.arrangeItems(this);
List<LookupElement> items = pair.first;
Integer toSelect = pair.second;
if (toSelect == null || toSelect < 0 || items.size() > 0 && toSelect >= items.size()) {
LOG.error("Arranger " + myPresentableArranger + " returned invalid selection index=" + toSelect + "; items=" + items);
}
if (!model.isEmpty()) {
for (LookupElement element : model) {
checkMinPrefixLengthChanges(items);
Object[] oldModel = listModel.toArray();
listModel.clear();
if (!items.isEmpty()) {
for (LookupElement element : items) {
listModel.addElement(element);
}
}
else {
addEmptyItem(listModel);
}
updateListHeight(listModel);
myList.setSelectedIndex(toSelect);
return Arrays.equals(oldModel, items.toArray());
}
updateListHeight(listModel);
if (!model.isEmpty()) {
LookupElement first = model.iterator().next();
if (isFocused() && (!isExactPrefixItem(first, true) || mySelectionTouched || shouldSkip(first))) {
restoreSelection(oldSelected, hasPreselected, oldInvariant, snapshot.second);
}
else {
myList.setSelectedIndex(0);
}
}
return snapshot.third;
}
private static boolean shouldSkip(LookupElement element) {
return element instanceof LiveTemplateLookupElement && ((LiveTemplateLookupElement)element).sudden;
}
private boolean isSelectionVisible() {
return myList.getFirstVisibleIndex() <= myList.getSelectedIndex() && myList.getSelectedIndex() <= myList.getLastVisibleIndex();
}
private LinkedHashSet<LookupElement> matchingItems(final List<LookupElement> elements) {
final LinkedHashSet<LookupElement> items = new LinkedHashSet<LookupElement>();
for (LookupElement element : elements) {
if (prefixMatches(element)) {
items.add(element);
}
}
return items;
}
private boolean checkReused() {
if (myPresentableModel != myModel) {
myAdditionalPrefix = "";
myFrozenItems.clear();
myPresentableModel = myModel;
return true;
synchronized (myList) {
if (myPresentableArranger != myArranger) {
myPresentableArranger = myArranger;
myPresentableArranger.prefixChanged();
return true;
}
return false;
}
return false;
}
private void checkMinPrefixLengthChanges(Collection<LookupElement> items) {
@@ -554,39 +495,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
updateLookupStart(minPrefixLength);
}
private void restoreSelection(@Nullable LookupElement oldSelected, boolean choosePreselectedItem, @Nullable String oldInvariant, Iterable<List<LookupElement>> groups) {
if (oldSelected != null) {
if (oldSelected.isValid()) {
myList.setSelectedValue(oldSelected, false);
if (myList.getSelectedValue() == oldSelected) {
return;
}
}
if (oldInvariant != null) {
for (LookupElement element : getItems()) {
if (oldInvariant.equals(myPresentableModel.getItemPresentationInvariant(element))) {
myList.setSelectedValue(element, false);
if (myList.getSelectedValue() == element) {
return;
}
}
}
}
}
LookupElement preselected = myPresentableModel.preselectedItem;
if (choosePreselectedItem) {
myList.setSelectedValue(preselected, false);
} else {
myList.setSelectedIndex(doSelectMostPreferableItem(getItems(), groups));
}
if (preselected != null && myShown) {
myPresentableModel.preselectedItem = getCurrentItem();
}
}
private void updateListHeight(ListModel model) {
myList.setFixedCellHeight(myCellRenderer.getListCellRendererComponent(myList, model.getElementAt(0), 0, false, false).getPreferredSize().height);
@@ -608,85 +516,19 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
return p;
}
private void addMostRelevantItems(final Set<LookupElement> model, final Iterable<List<LookupElement>> sortedItems) {
if (model.size() > MAX_PREFERRED_COUNT) return;
for (final List<LookupElement> elements : sortedItems) {
final List<LookupElement> suitable = new SmartList<LookupElement>();
for (final LookupElement item : elements) {
if (!model.contains(item) && prefixMatches(item)) {
suitable.add(item);
}
}
if (model.size() + suitable.size() > MAX_PREFERRED_COUNT) break;
model.addAll(suitable);
}
}
public boolean isFrozen(@NotNull LookupElement element) {
return myFrozenItems.contains(element);
}
private List<LookupElement> getPrefixItems(final Collection<LookupElement> elements, final boolean caseSensitive) {
List<LookupElement> better = new ArrayList<LookupElement>();
for (LookupElement element : elements) {
if (isExactPrefixItem(element, caseSensitive)) {
better.add(element);
}
}
final Comparator<LookupElement> itemComparator = myPresentableModel.getArranger().getItemComparator();
if (itemComparator != null) {
Collections.sort(better, itemComparator);
}
List<LookupElement> classified = myPresentableModel.classifyByRelevance(better);
List<LookupElement> result = new ArrayList<LookupElement>(classified.size());
for (LookupElement element : classified) {
if (element.getLookupString().equals(itemPattern(element))) {
result.add(element);
}
}
for (LookupElement element : classified) {
if (!element.getLookupString().equals(itemPattern(element))) {
result.add(element);
}
}
return result;
}
@NotNull
@Override
public String itemPattern(LookupElement element) {
public String itemPattern(@NotNull LookupElement element) {
return itemMatcher(element).getPrefix() + myAdditionalPrefix;
}
private boolean isAlphaSorted() {
return isCompletion() && UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
}
private boolean isExactPrefixItem(LookupElement item, final boolean caseSensitive) {
final String pattern = itemPattern(item);
final Set<String> strings = item.getAllLookupStrings();
if (strings.contains(pattern)) {
return caseSensitive; //to not add the same elements twice to the model, as sensitive and then as insensitive
}
if (caseSensitive) {
@Override
public boolean prefixMatches(@NotNull final LookupElement item) {
PrefixMatcher matcher = myMatchers.get(item);
if (matcher == null) {
return false;
}
for (String s : strings) {
if (s.equalsIgnoreCase(pattern)) {
return true;
}
}
return false;
}
private boolean prefixMatches(final LookupElement item) {
PrefixMatcher matcher = itemMatcher(item);
if (myAdditionalPrefix.length() > 0) {
matcher = matcher.cloneWithPrefix(itemPattern(item));
}
@@ -695,7 +537,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
@Override
@NotNull
public PrefixMatcher itemMatcher(LookupElement item) {
public PrefixMatcher itemMatcher(@NotNull LookupElement item) {
PrefixMatcher matcher = myMatchers.get(item);
if (matcher == null) {
throw new AssertionError("Item not in lookup: item=" + item + "; lookup items=" + getItems());
@@ -1032,7 +874,6 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
final LookupElement item = getCurrentItem();
if (oldItem != item) {
mySelectionInvariant = item == null ? null : myPresentableModel.getItemPresentationInvariant(item);
fireCurrentItemChanged(item);
if (myDisposed) { //a listener may have decided to close us, what can we do?
return;
@@ -1266,9 +1107,17 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
final int start = offset - presentPrefix.length();
myEditor.getDocument().replaceString(start, offset, newPrefix);
Map<LookupElement, PrefixMatcher> newItems = myPresentableModel.retainMatchingItems(newPrefix, LookupImpl.this);
Map<LookupElement, PrefixMatcher> newMatchers = new HashMap<LookupElement, PrefixMatcher>();
for (LookupElement item : getItems()) {
if (item.isValid()) {
PrefixMatcher matcher = itemMatcher(item).cloneWithPrefix(newPrefix);
if (matcher.prefixMatches(item)) {
newMatchers.put(item, matcher);
}
}
}
myMatchers.clear();
myMatchers.putAll(newItems);
myMatchers.putAll(newMatchers);
myAdditionalPrefix = "";
@@ -1286,7 +1135,7 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
}
public boolean isCompletion() {
return myModel.getArranger() instanceof CompletionLookupArranger;
return myArranger instanceof CompletionLookupArranger;
}
public PsiElement getPsiElement() {
@@ -1388,29 +1237,11 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
staticDisposeTrace = disposeTrace;
}
private int doSelectMostPreferableItem(List<LookupElement> items, Iterable<List<LookupElement>> groups) {
if (items.isEmpty()) {
return -1;
}
if (items.size() == 1) {
return 0;
}
for (int i = 0; i < items.size(); i++) {
LookupElement item = items.get(i);
if (isExactPrefixItem(item, true) && !shouldSkip(item)) {
return i;
}
}
final int index = myModel.getArranger().suggestPreselectedItem(items, groups);
assert index >= 0 && index < items.size();
return index;
}
public void refreshUi(boolean mayCheckReused) {
final boolean reused = mayCheckReused && checkReused();
if (reused) {
myAdditionalPrefix = "";
}
boolean selectionVisible = isSelectionVisible();
@@ -1457,7 +1288,9 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
public void markReused() {
myAdComponent.clearAdvertisements();
myModel = new LookupModel(null);
synchronized (myList) {
myArranger = myArranger.createEmptyCopy();
}
requestResize();
}
@@ -1614,8 +1447,10 @@ public class LookupImpl extends LightweightHint implements LookupEx, Disposable
}
}
public LinkedHashMap<LookupElement,StringBuilder> getRelevanceStrings() {
return myPresentableModel.getRelevanceStrings();
public Map<LookupElement,StringBuilder> getRelevanceStrings() {
synchronized (myList) {
return myPresentableArranger.getRelevanceStrings();
}
}
}
@@ -1,158 +0,0 @@
/*
* Copyright 2000-2010 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.lookup.impl;
import com.intellij.codeInsight.completion.PrefixMatcher;
import com.intellij.codeInsight.lookup.Classifier;
import com.intellij.codeInsight.lookup.LookupArranger;
import com.intellij.codeInsight.lookup.LookupElement;
import com.intellij.codeInsight.lookup.LookupElementPresentation;
import com.intellij.openapi.util.Trinity;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.SortedList;
import gnu.trove.THashMap;
import gnu.trove.TObjectHashingStrategy;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* @author peter
*/
public class LookupModel {
private static final Comparator<LookupElement> COMMUNISM = new Comparator<LookupElement>() {
@SuppressWarnings({"ComparatorMethodParameterNotUsed"})
@Override
public int compare(LookupElement o1, LookupElement o2) {
return 0;
}
};
private final Object lock = new Object();
@SuppressWarnings({"unchecked"}) private final Map<LookupElement, String> myItemPresentations = new THashMap<LookupElement, String>(TObjectHashingStrategy.IDENTITY);
private final List<LookupElement> myItems = new ArrayList<LookupElement>();
private SortedList<LookupElement> mySortedItems;
private LookupArranger myArranger;
private Classifier<LookupElement> myRelevanceClassifier;
@Nullable public LookupElement preselectedItem;
private int stamp;
private int lastAccess;
public LookupModel(LookupElement preselectedItem) {
this.preselectedItem = preselectedItem;
}
public List<LookupElement> getItems() {
synchronized (lock) {
return new ArrayList<LookupElement>(myItems);
}
}
public void clearItems() {
synchronized (lock) {
myItems.clear();
mySortedItems.clear();
myRelevanceClassifier = myArranger.createRelevanceClassifier();
}
}
public void addItem(LookupElement item) {
synchronized (lock) {
myRelevanceClassifier.addElement(item);
mySortedItems.add(item); // ProcessCanceledException may occur in these two lines, then this element is considered not added
myItems.add(item);
stamp++;
}
}
public void setItemPresentation(LookupElement item, LookupElementPresentation presentation) {
final String invariant = presentation.getItemText() + "###" + presentation.getTailText() + "###" + presentation.getTypeText();
synchronized (lock) {
myItemPresentations.put(item, invariant);
}
}
public String getItemPresentationInvariant(LookupElement element) {
synchronized (lock) {
return myItemPresentations.get(element);
}
}
public Trinity<List<LookupElement>, Iterable<List<LookupElement>>, Boolean> getModelSnapshot() {
synchronized (lock) {
final List<LookupElement> sorted = new ArrayList<LookupElement>(mySortedItems);
final Iterable<List<LookupElement>> groups = myRelevanceClassifier.classify(sorted);
boolean changed = lastAccess != stamp;
lastAccess = stamp;
return Trinity.create(sorted, groups, changed);
}
}
@Nullable
Map<LookupElement, PrefixMatcher> retainMatchingItems(final String newPrefix, final LookupImpl lookup) {
synchronized (lock) {
LinkedHashMap<LookupElement, PrefixMatcher> map = new LinkedHashMap<LookupElement, PrefixMatcher>();
for (LookupElement item : myItems) {
if (item.isValid()) {
PrefixMatcher matcher = lookup.itemMatcher(item).cloneWithPrefix(newPrefix);
if (matcher.prefixMatches(item)) {
map.put(item, matcher);
}
}
}
if (map.size() != myItems.size()) {
clearItems();
for (LookupElement newItem : map.keySet()) {
addItem(newItem);
}
}
return map;
}
}
public void setArranger(final LookupArranger arranger) {
synchronized (lock) {
myArranger = arranger;
final Comparator<LookupElement> comparator = arranger.getItemComparator();
mySortedItems = new SortedList<LookupElement>(comparator == null ? COMMUNISM : comparator);
myRelevanceClassifier = myArranger.createRelevanceClassifier();
}
}
LinkedHashMap<LookupElement,StringBuilder> getRelevanceStrings() {
final LinkedHashMap<LookupElement,StringBuilder> map = new LinkedHashMap<LookupElement, StringBuilder>();
for (LookupElement item : myItems) {
map.put(item, new StringBuilder());
}
myRelevanceClassifier.describeItems(map);
return map;
}
public List<LookupElement> classifyByRelevance(List<LookupElement> list) {
synchronized (lock) {
return ContainerUtil.flatten(myRelevanceClassifier.classify(list));
}
}
LookupArranger getArranger() {
synchronized (lock) {
return myArranger;
}
}
}
@@ -31,6 +31,7 @@ import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiFile;
import com.intellij.util.containers.CollectionFactory;
@@ -78,31 +79,8 @@ public class ListTemplatesHandler implements CodeInsightActionHandler {
public static void showTemplatesLookup(final Project project, final Editor editor,
@NotNull String prefix, List<TemplateImpl> matchingTemplates) {
final LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, prefix, LookupArranger.DEFAULT);
lookup.setArranger(new LookupArranger() {
/*
@Override
public Comparator<LookupElement> getItemComparator() {
return new Comparator<LookupElement>() {
@Override
public int compare(LookupElement o1, LookupElement o2) {
return o1.getLookupString().compareToIgnoreCase(o2.getLookupString());
}
};
}
*/
@Override
public Classifier<LookupElement> createRelevanceClassifier() {
return new ComparingClassifier<LookupElement>(ClassifierFactory.<LookupElement>listClassifier(), "preferPrefix") {
@NotNull
@Override
public Comparable getWeight(LookupElement element) {
return !element.getLookupString().startsWith(lookup.itemPattern(element));
}
};
}
});
final LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, prefix,
new TemplatesArranger());
for (TemplateImpl template : matchingTemplates) {
lookup.addItem(createTemplateElement(template), new PlainPrefixMatcher(prefix));
}
@@ -135,7 +113,8 @@ public class ListTemplatesHandler implements CodeInsightActionHandler {
}
public static void showTemplatesLookup(final Project project, final Editor editor, Map<TemplateImpl, String> template2Argument) {
final LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "", LookupArranger.DEFAULT);
final LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, "",
new LookupArranger.DefaultArranger());
for (TemplateImpl template : template2Argument.keySet()) {
String prefix = computePrefix(template, template2Argument.get(template));
lookup.addItem(createTemplateElement(template), new PlainPrefixMatcher(prefix));
@@ -197,4 +176,28 @@ public class ListTemplatesHandler implements CodeInsightActionHandler {
}
}
}
private static class TemplatesArranger extends LookupArranger {
@Override
public Pair<List<LookupElement>, Integer> arrangeItems(@NotNull Lookup lookup) {
LinkedHashSet<LookupElement> result = new LinkedHashSet<LookupElement>();
List<LookupElement> items = matchingItems(lookup);
for (LookupElement item : items) {
if (item.getLookupString().startsWith(lookup.itemPattern(item))) {
result.add(item);
}
}
result.addAll(items);
ArrayList<LookupElement> list = new ArrayList<LookupElement>(result);
int selected = list.indexOf(lookup.getCurrentItem());
return new Pair<List<LookupElement>, Integer>(list, selected >= 0 ? selected : 0);
}
@Override
public LookupArranger createEmptyCopy() {
return new TemplatesArranger();
}
}
}
@@ -29,8 +29,8 @@ import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.util.text.StringUtil;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* @author peter
@@ -58,11 +58,11 @@ public class DumpLookupElementWeights extends AnAction implements DumbAware {
}
public static List<String> getLookupElementWeights(LookupImpl lookup) {
final LinkedHashMap<LookupElement,StringBuilder> strings = lookup.getRelevanceStrings();
final Map<LookupElement,StringBuilder> strings = lookup.getRelevanceStrings();
List<String> sb = new ArrayList<String>();
for (LookupElement item : lookup.getItems()) {
String weight = strings.get(item).toString();
final String s = item.getLookupString() + (lookup.isFrozen(item) ? "\t_first_\t" : "\t") + weight;
final String s = item.getLookupString() + "\t" + weight;
sb.add(s);
}
return sb;