farewell green items in completion
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / completion / CompletionLookupArranger.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
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
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
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.
15  */
16
17 package com.intellij.codeInsight.completion;
18
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;
44
45 import java.util.*;
46
47 public class CompletionLookupArranger extends LookupArranger {
48   @Nullable private static StatisticsUpdate ourPendingUpdate;
49   private static final Alarm ourStatsAlarm = new Alarm(ApplicationManager.getApplication());
50   
51   static {
52     Disposer.register(ApplicationManager.getApplication(), new Disposable() {
53       @Override
54       public void dispose() {
55         cancelLastCompletionStatisticsUpdate();
56       }
57     });
58   }
59
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;
65
66   public CompletionLookupArranger(final CompletionParameters parameters, CompletionProgressIndicator process) {
67     myProcess = process;
68     myLocation = new CompletionLocation(parameters);
69   }
70
71   @Override
72   @NotNull
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));
78       }
79     };
80   }
81
82   public static StatisticsUpdate collectStatisticChanges(CompletionProgressIndicator indicator, LookupElement item) {
83     LookupImpl lookupImpl = indicator.getLookup();
84     applyLastCompletionStatisticsUpdate();
85
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(3, lookupImpl.getList().getSelectedIndex());
90
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));
97       }
98     }
99
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;
103
104     StatisticsUpdate update = new StatisticsUpdate(ignored, selected, main);
105     ourPendingUpdate = update;
106     Disposer.register(update, new Disposable() {
107       @Override
108       public void dispose() {
109         //noinspection AssignmentToStaticFieldFromInstanceMethod
110         ourPendingUpdate = null;
111       }
112     });
113
114     return update;
115   }
116
117   public static void trackStatistics(InsertionContext context, final StatisticsUpdate update) {
118     if (ourPendingUpdate != update) {
119       return;
120     }
121
122     final Document document = context.getDocument();
123     int startOffset = context.getStartOffset();
124     int tailOffset = context.getEditor().getCaretModel().getOffset();
125     if (startOffset < 0 || tailOffset <= startOffset) {
126       return;
127     }
128
129     final RangeMarker marker = document.createRangeMarker(startOffset, tailOffset);
130     final DocumentAdapter listener = new DocumentAdapter() {
131       @Override
132       public void beforeDocumentChange(DocumentEvent e) {
133         if (!marker.isValid() || e.getOffset() > marker.getStartOffset() && e.getOffset() < marker.getEndOffset()) {
134           cancelLastCompletionStatisticsUpdate();
135         }
136       }
137     };
138
139     ourStatsAlarm.addRequest(new Runnable() {
140       @Override
141       public void run() {
142         if (ourPendingUpdate == update) {
143           applyLastCompletionStatisticsUpdate();
144         }
145       }
146     }, 20 * 1000);
147
148     document.addDocumentListener(listener);
149     Disposer.register(update, new Disposable() {
150       @Override
151       public void dispose() {
152         document.removeDocumentListener(listener);
153         marker.dispose();
154         ourStatsAlarm.cancelAllRequests();
155       }
156     });
157   }
158
159   public static void cancelLastCompletionStatisticsUpdate() {
160     if (ourPendingUpdate != null) {
161       Disposer.dispose(ourPendingUpdate);
162       assert ourPendingUpdate == null;
163     }
164   }
165
166   public static void applyLastCompletionStatisticsUpdate() {
167     StatisticsUpdate update = ourPendingUpdate;
168     if (update != null) {
169       update.performUpdate();
170       Disposer.dispose(update);
171       assert ourPendingUpdate == null;
172     }
173   }
174
175   public int suggestPreselectedItem(List<LookupElement> sorted, Iterable<List<LookupElement>> groups) {
176     final CompletionPreselectSkipper[] skippers = CompletionPreselectSkipper.EP_NAME.getExtensions();
177
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);
184           }
185         }
186       }
187     }
188
189     return sorted.size() - 1;
190   }
191
192   private boolean shouldSkip(CompletionPreselectSkipper[] skippers, LookupElement element) {
193     for (final CompletionPreselectSkipper skipper : skippers) {
194       if (skipper.skipElement(element, myLocation)) {
195         return true;
196       }
197     }
198     return false;
199   }
200
201   public static String composeContextWithValue(final StatisticsInfo info) {
202     return info.getContext() + "###" + info.getValue();
203   }
204
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>>() {
209         @Override
210         protected Map<CompletionSorterImpl, Classifier<LookupElement>> createMap() {
211           return Maps.newLinkedHashMap();
212         }
213
214         @Override
215         protected Classifier<LookupElement> create(CompletionSorterImpl key) {
216           return key.buildClassifier();
217         }
218       };
219
220       @Override
221       public void addElement(LookupElement element) {
222         mySortingWeights.put(element, WeighingService.weigh(CompletionService.SORTING_KEY, element, myLocation));
223         myClassifiers.get(obtainSorter(element)).addElement(element);
224       }
225
226       @Override
227       public Iterable<List<LookupElement>> classify(List<LookupElement> source) {
228         MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(source);
229
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)));
233         }
234         return result;
235       }
236
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);
241         }
242         return inputBySorter;
243       }
244
245       @NotNull
246       private CompletionSorterImpl obtainSorter(LookupElement element) {
247         return myProcess.getSorter(element);
248       }
249
250       @Override
251       public void describeItems(LinkedHashMap<LookupElement, StringBuilder> map) {
252         final MultiMap<CompletionSorterImpl, LookupElement> inputBySorter = groupInputBySorter(new ArrayList<LookupElement>(map.keySet()));
253
254         if (inputBySorter.size() > 1) {
255           for (LookupElement element : map.keySet()) {
256             map.get(element).append(obtainSorter(element)).append(": ");
257           }
258         }
259
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));
264           }
265           myClassifiers.get(sorter).describeItems(subMap);
266         }
267       }
268     };
269   }
270
271   static class StatisticsUpdate implements Disposable {
272     private final List<StatisticsInfo> myIgnored;
273     private final StatisticsInfo mySelected;
274     private final StatisticsInfo myMain;
275
276     public StatisticsUpdate(List<StatisticsInfo> ignored, StatisticsInfo selected, StatisticsInfo main) {
277       myIgnored = ignored;
278       mySelected = selected;
279       myMain = main;
280     }
281
282     void performUpdate() {
283       for (StatisticsInfo statisticsInfo : myIgnored) {
284         StatisticsManager.getInstance().incUseCount(statisticsInfo);
285       }
286       if (mySelected != null) {
287         StatisticsManager.getInstance().incUseCount(mySelected);
288       }
289       if (myMain != null) {
290         StatisticsManager.getInstance().incUseCount(myMain);
291       }
292     }
293
294     @Override
295     public void dispose() {
296     }
297   }
298 }