mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
more control over lookup item order and selection
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
+2
-1
@@ -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);
|
||||
|
||||
+222
-91
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+29
-26
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user