From 3fc80399cd9bc9b5e49a13e31c7ae7a3501eacb9 Mon Sep 17 00:00:00 2001 From: peter Date: Tue, 24 Apr 2012 12:52:33 +0200 Subject: [PATCH] more control over lookup item order and selection --- .../intellij/codeInsight/lookup/Lookup.java | 6 +- .../completion/CodeCompletionHandlerBase.java | 3 +- .../completion/CompletionLookupArranger.java | 313 ++++++++++++----- .../codeInsight/lookup/LookupArranger.java | 92 ++++- .../codeInsight/lookup/LookupManager.java | 4 +- .../codeInsight/lookup/impl/LookupImpl.java | 325 +++++------------- .../codeInsight/lookup/impl/LookupModel.java | 158 --------- .../template/impl/ListTemplatesHandler.java | 55 +-- .../internal/DumpLookupElementWeights.java | 6 +- 9 files changed, 417 insertions(+), 545 deletions(-) delete mode 100644 platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupModel.java diff --git a/platform/lang-api/src/com/intellij/codeInsight/lookup/Lookup.java b/platform/lang-api/src/com/intellij/codeInsight/lookup/Lookup.java index a833495f785e..13123d3eacec 100644 --- a/platform/lang-api/src/com/intellij/codeInsight/lookup/Lookup.java +++ b/platform/lang-api/src/com/intellij/codeInsight/lookup/Lookup.java @@ -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); } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java b/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java index 49bc7983a534..ed8607bbf15d 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/completion/CodeCompletionHandlerBase.java @@ -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); diff --git a/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionLookupArranger.java b/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionLookupArranger.java index e7a9e1b5aa14..7003ad2c717b 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionLookupArranger.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/completion/CompletionLookupArranger.java @@ -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 PRESENTATION_INVARIANT = Key.create("PRESENTATION_INVARIANT"); + private final List myFrozenItems = new ArrayList(); + 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 mySortingWeights = new THashMap(TObjectHashingStrategy.IDENTITY); + @SuppressWarnings("unchecked") private final Map mySortingWeights = new THashMap(TObjectHashingStrategy.IDENTITY); + private final CompletionParameters myParameters; private final CompletionProgressIndicator myProcess; + private final Classifier myClassifier; public CompletionLookupArranger(final CompletionParameters parameters, CompletionProgressIndicator process) { + myParameters = parameters; myProcess = process; myLocation = new CompletionLocation(parameters); + myClassifier = new Classifier() { + @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"}) + private final FactoryMap> myClassifiers = + new FactoryMap>() { + @Override + protected Map> createMap() { + return Maps.newLinkedHashMap(); + } + + @Override + protected Classifier 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> classify(List source) { + MultiMap inputBySorter = groupInputBySorter(source); + + final ArrayList> result = new ArrayList>(); + for (CompletionSorterImpl sorter : myClassifiers.keySet()) { + ContainerUtil.addAll(result, myClassifiers.get(sorter).classify((List)inputBySorter.get(sorter))); + } + return result; + } + + private MultiMap groupInputBySorter(List source) { + MultiMap inputBySorter = new MultiMap(); + 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 map) { + final MultiMap inputBySorter = groupInputBySorter(new ArrayList(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 subMap = new LinkedHashMap(); + for (LookupElement element : inputBySorter.get(sorter)) { + subMap.put(element, map.get(element)); + } + myClassifiers.get(sorter).describeItems(subMap); + } + } + }; } @Override - @NotNull - public Comparator getItemComparator() { - return new Comparator() { + public Map getRelevanceStrings() { + final LinkedHashMap map = new LinkedHashMap(); + 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, Integer> arrangeItems(@NotNull Lookup lookup) { + List items = matchingItems(lookup); + Collections.sort(items, new Comparator() { public int compare(LookupElement o1, LookupElement o2) { //noinspection unchecked return mySortingWeights.get(o1).compareTo(mySortingWeights.get(o2)); } - }; + }); + Iterable> groups = myClassifier.classify(items); + + LinkedHashSet model = new LinkedHashSet(); + for (List group : groups) { + addPrefixItems(lookup, model, true, group); + } + for (List 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() { + @Override + public int compare(LookupElement o1, LookupElement o2) { + return o1.getLookupString().compareToIgnoreCase(o2.getLookupString()); + } + }); + model.addAll(items); + } else { + for (List group : groups) { + model.addAll(group); + } + } + ArrayList listModel = new ArrayList(model); + + return new Pair, Integer>(listModel, getItemToSelect(lookup, groups, listModel)); } + @Override + public LookupArranger createEmptyCopy() { + return new CompletionLookupArranger(myParameters, myProcess); + } + + private int getItemToSelect(Lookup lookup, Iterable> groups, List 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 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 model, final Iterable> sortedItems) { + if (model.size() > MAX_PREFERRED_COUNT) return; + + for (final List elements : sortedItems) { + final List suitable = new SmartList(); + 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 sorted, Iterable> groups) { - final CompletionPreselectSkipper[] skippers = CompletionPreselectSkipper.EP_NAME.getExtensions(); - - Set model = new THashSet(sorted); - for (List 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 createRelevanceClassifier() { - return new Classifier() { - @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"}) - private final FactoryMap> myClassifiers = new FactoryMap>() { - @Override - protected Map> createMap() { - return Maps.newLinkedHashMap(); - } - - @Override - protected Classifier 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> classify(List source) { - MultiMap inputBySorter = groupInputBySorter(source); - - final ArrayList> result = new ArrayList>(); - for (CompletionSorterImpl sorter : myClassifiers.keySet()) { - ContainerUtil.addAll(result, myClassifiers.get(sorter).classify((List)inputBySorter.get(sorter))); - } - return result; - } - - private MultiMap groupInputBySorter(List source) { - MultiMap inputBySorter = new MultiMap(); - 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 map) { - final MultiMap inputBySorter = groupInputBySorter(new ArrayList(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 subMap = new LinkedHashMap(); - 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 { diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupArranger.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupArranger.java index b04df9306ef4..aa558fdb49c4 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupArranger.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupArranger.java @@ -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 myItems = new ArrayList(); + + public void addElement(Lookup lookup, LookupElement item, LookupElementPresentation presentation) { + myItems.add(item); + } + + public void prefixChanged() { + } + + public abstract Pair, Integer> arrangeItems(@NotNull Lookup lookup); + + public final void clearItems() { + prefixChanged(); + myItems.clear(); + } + + public abstract LookupArranger createEmptyCopy(); + + protected static void addPrefixItems(Lookup lookup, LinkedHashSet result, boolean caseSensitive, Collection 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 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 matchingItems(Lookup lookup) { + final List items = new ArrayList(); + for (LookupElement element : myItems) { + if (lookup.prefixMatches(element)) { + items.add(element); + } + } + return items; + } + + public Map getRelevanceStrings() { + return Collections.emptyMap(); + } + + public static class DefaultArranger extends LookupArranger { + public Pair, Integer> arrangeItems(@NotNull Lookup lookup) { + LinkedHashSet result = new LinkedHashSet(); + List items = matchingItems(lookup); + addPrefixItems(lookup, result, true, items); + addPrefixItems(lookup, result, false, items); + result.addAll(items); + ArrayList list = new ArrayList(result); + int selected = list.indexOf(lookup.getCurrentItem()); + return new Pair, Integer>(list, selected >= 0 ? selected : 0); + } @Override - public Classifier createRelevanceClassifier() { - return ClassifierFactory.listClassifier(); + public LookupArranger createEmptyCopy() { + return new DefaultArranger(); } - }; - - public int suggestPreselectedItem(List sorted, Iterable> groups) { - return 0; - } - - public abstract Classifier createRelevanceClassifier(); - - @Nullable - public Comparator getItemComparator() { - return null; //don't sort } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupManager.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupManager.java index 02592e7b3313..a1a95fd65e09 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupManager.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/LookupManager.java @@ -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 diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java index 3ce12b1818a3..d3ff90f9170f 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupImpl.java @@ -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 myFrozenItems = new ArrayList(); - 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 myMatchers = new ConcurrentHashMap(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 items = getItems(); + synchronized (myList) { + myPresentableArranger.prefixChanged(); ((DefaultListModel)myList.getModel()).clear(); } - final List 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, Iterable>, Boolean> snapshot = myPresentableModel.getModelSnapshot(); - - final LinkedHashSet items = matchingItems(snapshot.first); - - checkMinPrefixLengthChanges(items); - - boolean hasPreselected = !mySelectionTouched && items.contains(myPresentableModel.preselectedItem); - LookupElement oldSelected = mySelectionTouched ? (LookupElement)myList.getSelectedValue() : null; - String oldInvariant = mySelectionInvariant; - - LinkedHashSet model = new LinkedHashSet(); - 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 elements = new ArrayList(items); - Collections.sort(elements, new Comparator() { - @Override - public int compare(LookupElement o1, LookupElement o2) { - return o1.getLookupString().compareToIgnoreCase(o2.getLookupString()); - } - }); - model.addAll(elements); - } else { - for (List group : snapshot.second) { - for (LookupElement element : group) { - if (prefixMatches(element)) { - model.add(element); - } - } - } - } - DefaultListModel listModel = (DefaultListModel)myList.getModel(); synchronized (myList) { - listModel.clear(); + Pair, Integer> pair = myPresentableArranger.arrangeItems(this); + List 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 matchingItems(final List elements) { - final LinkedHashSet items = new LinkedHashSet(); - 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 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> 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 model, final Iterable> sortedItems) { - if (model.size() > MAX_PREFERRED_COUNT) return; - - for (final List elements : sortedItems) { - final List suitable = new SmartList(); - 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 getPrefixItems(final Collection elements, final boolean caseSensitive) { - List better = new ArrayList(); - for (LookupElement element : elements) { - if (isExactPrefixItem(element, caseSensitive)) { - better.add(element); - } - } - - final Comparator itemComparator = myPresentableModel.getArranger().getItemComparator(); - if (itemComparator != null) { - Collections.sort(better, itemComparator); - } - - List classified = myPresentableModel.classifyByRelevance(better); - List result = new ArrayList(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 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 newItems = myPresentableModel.retainMatchingItems(newPrefix, LookupImpl.this); + Map newMatchers = new HashMap(); + 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 items, Iterable> 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 getRelevanceStrings() { - return myPresentableModel.getRelevanceStrings(); + public Map getRelevanceStrings() { + synchronized (myList) { + return myPresentableArranger.getRelevanceStrings(); + } } } diff --git a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupModel.java b/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupModel.java deleted file mode 100644 index db4e9cce94b5..000000000000 --- a/platform/lang-impl/src/com/intellij/codeInsight/lookup/impl/LookupModel.java +++ /dev/null @@ -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 COMMUNISM = new Comparator() { - @SuppressWarnings({"ComparatorMethodParameterNotUsed"}) - @Override - public int compare(LookupElement o1, LookupElement o2) { - return 0; - } - }; - private final Object lock = new Object(); - @SuppressWarnings({"unchecked"}) private final Map myItemPresentations = new THashMap(TObjectHashingStrategy.IDENTITY); - private final List myItems = new ArrayList(); - private SortedList mySortedItems; - private LookupArranger myArranger; - private Classifier myRelevanceClassifier; - @Nullable public LookupElement preselectedItem; - private int stamp; - private int lastAccess; - - public LookupModel(LookupElement preselectedItem) { - this.preselectedItem = preselectedItem; - } - - public List getItems() { - synchronized (lock) { - return new ArrayList(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, Iterable>, Boolean> getModelSnapshot() { - synchronized (lock) { - final List sorted = new ArrayList(mySortedItems); - final Iterable> groups = myRelevanceClassifier.classify(sorted); - boolean changed = lastAccess != stamp; - lastAccess = stamp; - return Trinity.create(sorted, groups, changed); - } - } - - @Nullable - Map retainMatchingItems(final String newPrefix, final LookupImpl lookup) { - synchronized (lock) { - LinkedHashMap map = new LinkedHashMap(); - 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 comparator = arranger.getItemComparator(); - mySortedItems = new SortedList(comparator == null ? COMMUNISM : comparator); - myRelevanceClassifier = myArranger.createRelevanceClassifier(); - } - } - - LinkedHashMap getRelevanceStrings() { - final LinkedHashMap map = new LinkedHashMap(); - for (LookupElement item : myItems) { - map.put(item, new StringBuilder()); - } - myRelevanceClassifier.describeItems(map); - return map; - } - - public List classifyByRelevance(List list) { - synchronized (lock) { - return ContainerUtil.flatten(myRelevanceClassifier.classify(list)); - } - } - - LookupArranger getArranger() { - synchronized (lock) { - return myArranger; - } - } -} diff --git a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/ListTemplatesHandler.java b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/ListTemplatesHandler.java index 7641fce4d3c0..f824802af91c 100644 --- a/platform/lang-impl/src/com/intellij/codeInsight/template/impl/ListTemplatesHandler.java +++ b/platform/lang-impl/src/com/intellij/codeInsight/template/impl/ListTemplatesHandler.java @@ -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 matchingTemplates) { - final LookupImpl lookup = (LookupImpl)LookupManager.getInstance(project).createLookup(editor, LookupElement.EMPTY_ARRAY, prefix, LookupArranger.DEFAULT); - lookup.setArranger(new LookupArranger() { - /* - @Override - public Comparator getItemComparator() { - return new Comparator() { - @Override - public int compare(LookupElement o1, LookupElement o2) { - return o1.getLookupString().compareToIgnoreCase(o2.getLookupString()); - } - }; - } - */ - - @Override - public Classifier createRelevanceClassifier() { - return new ComparingClassifier(ClassifierFactory.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 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, Integer> arrangeItems(@NotNull Lookup lookup) { + LinkedHashSet result = new LinkedHashSet(); + List items = matchingItems(lookup); + for (LookupElement item : items) { + if (item.getLookupString().startsWith(lookup.itemPattern(item))) { + result.add(item); + } + } + result.addAll(items); + ArrayList list = new ArrayList(result); + int selected = list.indexOf(lookup.getCurrentItem()); + return new Pair, Integer>(list, selected >= 0 ? selected : 0); + } + + @Override + public LookupArranger createEmptyCopy() { + return new TemplatesArranger(); + } + + } } diff --git a/platform/lang-impl/src/com/intellij/internal/DumpLookupElementWeights.java b/platform/lang-impl/src/com/intellij/internal/DumpLookupElementWeights.java index 5ccb4bdb7cb7..0db3f4a4570d 100644 --- a/platform/lang-impl/src/com/intellij/internal/DumpLookupElementWeights.java +++ b/platform/lang-impl/src/com/intellij/internal/DumpLookupElementWeights.java @@ -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 getLookupElementWeights(LookupImpl lookup) { - final LinkedHashMap strings = lookup.getRelevanceStrings(); + final Map strings = lookup.getRelevanceStrings(); List sb = new ArrayList(); 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;