2 * Copyright 2000-2009 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.intellij.codeInsight.completion;
19 import com.google.common.collect.Maps;
20 import com.intellij.codeInsight.completion.impl.CompletionSorterImpl;
21 import com.intellij.codeInsight.lookup.Classifier;
22 import com.intellij.codeInsight.lookup.LookupArranger;
23 import com.intellij.codeInsight.lookup.LookupElement;
24 import com.intellij.codeInsight.lookup.impl.LookupImpl;
25 import com.intellij.openapi.Disposable;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.editor.Document;
28 import com.intellij.openapi.editor.RangeMarker;
29 import com.intellij.openapi.editor.event.DocumentAdapter;
30 import com.intellij.openapi.editor.event.DocumentEvent;
31 import com.intellij.openapi.util.Disposer;
32 import com.intellij.psi.WeighingService;
33 import com.intellij.psi.statistics.StatisticsInfo;
34 import com.intellij.psi.statistics.StatisticsManager;
35 import com.intellij.util.Alarm;
36 import com.intellij.util.containers.ContainerUtil;
37 import com.intellij.util.containers.FactoryMap;
38 import com.intellij.util.containers.MultiMap;
39 import gnu.trove.THashMap;
40 import gnu.trove.THashSet;
41 import gnu.trove.TObjectHashingStrategy;
42 import org.jetbrains.annotations.NotNull;
43 import org.jetbrains.annotations.Nullable;
47 public class CompletionLookupArranger extends LookupArranger {
48 @Nullable private static StatisticsUpdate ourPendingUpdate;
49 private static final Alarm ourStatsAlarm = new Alarm(ApplicationManager.getApplication());
52 Disposer.register(ApplicationManager.getApplication(), new Disposable() {
54 public void dispose() {
55 cancelLastCompletionStatisticsUpdate();
60 private static final String SELECTED = "selected";
61 static final String IGNORED = "ignored";
62 private final CompletionLocation myLocation;
63 private final Map<LookupElement, Comparable> mySortingWeights = new THashMap<LookupElement, Comparable>(TObjectHashingStrategy.IDENTITY);
64 private final CompletionProgressIndicator myProcess;
66 public CompletionLookupArranger(final CompletionParameters parameters, CompletionProgressIndicator process) {
68 myLocation = new CompletionLocation(parameters);
73 public Comparator<LookupElement> getItemComparator() {
74 return new Comparator<LookupElement>() {
75 public int compare(LookupElement o1, LookupElement o2) {
76 //noinspection unchecked
77 return mySortingWeights.get(o1).compareTo(mySortingWeights.get(o2));
82 public static StatisticsUpdate collectStatisticChanges(CompletionProgressIndicator indicator, LookupElement item) {
83 LookupImpl lookupImpl = indicator.getLookup();
84 applyLastCompletionStatisticsUpdate();
86 CompletionLocation myLocation = new CompletionLocation(indicator.getParameters());
87 final StatisticsInfo main = StatisticsManager.serialize(CompletionService.STATISTICS_KEY, item, myLocation);
88 final List<LookupElement> items = lookupImpl.getItems();
89 final int count = Math.min(lookupImpl.getPreferredItemsCount(), lookupImpl.getList().getSelectedIndex());
91 final List<StatisticsInfo> ignored = new ArrayList<StatisticsInfo>();
92 for (int i = 0; i < count; i++) {
93 final LookupElement element = items.get(i);
94 StatisticsInfo baseInfo = StatisticsManager.serialize(CompletionService.STATISTICS_KEY, element, myLocation);
95 if (baseInfo != null && baseInfo != StatisticsInfo.EMPTY && StatisticsManager.getInstance().getUseCount(baseInfo) == 0) {
96 ignored.add(new StatisticsInfo(composeContextWithValue(baseInfo), IGNORED));
100 StatisticsInfo info = StatisticsManager.serialize(CompletionService.STATISTICS_KEY, item, myLocation);
101 final StatisticsInfo selected =
102 info != null && info != StatisticsInfo.EMPTY ? new StatisticsInfo(composeContextWithValue(info), SELECTED) : null;
104 StatisticsUpdate update = new StatisticsUpdate(ignored, selected, main);
105 ourPendingUpdate = update;
106 Disposer.register(update, new Disposable() {
108 public void dispose() {
109 //noinspection AssignmentToStaticFieldFromInstanceMethod
110 ourPendingUpdate = null;
117 public static void trackStatistics(InsertionContext context, final StatisticsUpdate update) {
118 if (ourPendingUpdate != update) {
122 final Document document = context.getDocument();
123 int startOffset = context.getStartOffset();
124 int tailOffset = context.getEditor().getCaretModel().getOffset();
125 if (startOffset < 0 || tailOffset <= startOffset) {
129 final RangeMarker marker = document.createRangeMarker(startOffset, tailOffset);
130 final DocumentAdapter listener = new DocumentAdapter() {
132 public void beforeDocumentChange(DocumentEvent e) {
133 if (!marker.isValid() || e.getOffset() > marker.getStartOffset() && e.getOffset() < marker.getEndOffset()) {
134 cancelLastCompletionStatisticsUpdate();
139 ourStatsAlarm.addRequest(new Runnable() {
142 if (ourPendingUpdate == update) {
143 applyLastCompletionStatisticsUpdate();
148 document.addDocumentListener(listener);
149 Disposer.register(update, new Disposable() {
151 public void dispose() {
152 document.removeDocumentListener(listener);
154 ourStatsAlarm.cancelAllRequests();
159 public static void cancelLastCompletionStatisticsUpdate() {
160 if (ourPendingUpdate != null) {
161 Disposer.dispose(ourPendingUpdate);
162 assert ourPendingUpdate == null;
166 public static void applyLastCompletionStatisticsUpdate() {
167 StatisticsUpdate update = ourPendingUpdate;
168 if (update != null) {
169 update.performUpdate();
170 Disposer.dispose(update);
171 assert ourPendingUpdate == null;
175 public int suggestPreselectedItem(List<LookupElement> sorted, Iterable<List<LookupElement>> groups) {
176 final CompletionPreselectSkipper[] skippers = CompletionPreselectSkipper.EP_NAME.getExtensions();
178 Set<LookupElement> model = new THashSet<LookupElement>(sorted);
179 for (List<LookupElement> group : groups) {
180 for (LookupElement element : group) {
181 if (model.contains(element)) {
182 if (!shouldSkip(skippers, element)) {
183 return sorted.indexOf(element);
189 return sorted.size() - 1;
192 private boolean shouldSkip(CompletionPreselectSkipper[] skippers, LookupElement element) {
193 for (final CompletionPreselectSkipper skipper : skippers) {
194 if (skipper.skipElement(element, myLocation)) {
201 public static String composeContextWithValue(final StatisticsInfo info) {
202 return info.getContext() + "###" + info.getValue();
205 public Classifier<LookupElement> createRelevanceClassifier() {
206 return new Classifier<LookupElement>() {
207 @SuppressWarnings({"MismatchedQueryAndUpdateOfCollection"})
208 private final FactoryMap<CompletionSorterImpl, Classifier<LookupElement>> myClassifiers = new FactoryMap<CompletionSorterImpl, Classifier<LookupElement>>() {
210 protected Map<CompletionSorterImpl, Classifier<LookupElement>> createMap() {
211 return Maps.newLinkedHashMap();
215 protected Classifier<LookupElement> create(CompletionSorterImpl key) {
216 return key.buildClassifier();
221 public void addElement(LookupElement element) {
222 mySortingWeights.put(element, WeighingService.weigh(CompletionService.SORTING_KEY, element, myLocation));
223 myClassifiers.get(obtainSorter(element)).addElement(element);
227 public Iterable<List<LookupElement>> classify(List<LookupElement> source) {
228 MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(source);
230 final ArrayList<List<LookupElement>> result = new ArrayList<List<LookupElement>>();
231 for (CompletionSorterImpl sorter : myClassifiers.keySet()) {
232 ContainerUtil.addAll(result, myClassifiers.get(sorter).classify((List<LookupElement>)inputBySorter.get(sorter)));
237 private MultiMap<CompletionSorterImpl, LookupElement> groupInputBySorter(List<LookupElement> source) {
238 MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = new MultiMap<CompletionSorterImpl, LookupElement>();
239 for (LookupElement element : source) {
240 inputBySorter.putValue(obtainSorter(element), element);
242 return inputBySorter;
246 private CompletionSorterImpl obtainSorter(LookupElement element) {
247 return myProcess.getSorter(element);
251 public void describeItems(LinkedHashMap<LookupElement, StringBuilder> map) {
252 final MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(new ArrayList<LookupElement>(map.keySet()));
254 if (inputBySorter.size() > 1) {
255 for (LookupElement element : map.keySet()) {
256 map.get(element).append(obtainSorter(element)).append(": ");
260 for (CompletionSorterImpl sorter : inputBySorter.keySet()) {
261 final LinkedHashMap<LookupElement, StringBuilder> subMap = new LinkedHashMap<LookupElement, StringBuilder>();
262 for (LookupElement element : inputBySorter.get(sorter)) {
263 subMap.put(element, map.get(element));
265 myClassifiers.get(sorter).describeItems(subMap);
271 static class StatisticsUpdate implements Disposable {
272 private final List<StatisticsInfo> myIgnored;
273 private final StatisticsInfo mySelected;
274 private final StatisticsInfo myMain;
276 public StatisticsUpdate(List<StatisticsInfo> ignored, StatisticsInfo selected, StatisticsInfo main) {
278 mySelected = selected;
282 void performUpdate() {
283 for (StatisticsInfo statisticsInfo : myIgnored) {
284 StatisticsManager.getInstance().incUseCount(statisticsInfo);
286 if (mySelected != null) {
287 StatisticsManager.getInstance().incUseCount(mySelected);
289 if (myMain != null) {
290 StatisticsManager.getInstance().incUseCount(myMain);
295 public void dispose() {