cleanup
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / lookup / impl / LookupImpl.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.lookup.impl;
18
19 import com.intellij.codeInsight.CodeInsightBundle;
20 import com.intellij.codeInsight.completion.CodeCompletionFeatures;
21 import com.intellij.codeInsight.completion.CompletionLookupArranger;
22 import com.intellij.codeInsight.completion.PrefixMatcher;
23 import com.intellij.codeInsight.completion.RangeMarkerSpy;
24 import com.intellij.codeInsight.completion.impl.CamelHumpMatcher;
25 import com.intellij.codeInsight.hint.HintManager;
26 import com.intellij.codeInsight.hint.HintManagerImpl;
27 import com.intellij.codeInsight.lookup.*;
28 import com.intellij.codeInsight.template.impl.LiveTemplateLookupElement;
29 import com.intellij.featureStatistics.FeatureUsageTracker;
30 import com.intellij.ide.ui.UISettings;
31 import com.intellij.lang.LangBundle;
32 import com.intellij.openapi.Disposable;
33 import com.intellij.openapi.actionSystem.ActionManager;
34 import com.intellij.openapi.actionSystem.IdeActions;
35 import com.intellij.openapi.application.AccessToken;
36 import com.intellij.openapi.application.ApplicationManager;
37 import com.intellij.openapi.application.WriteAction;
38 import com.intellij.openapi.command.CommandProcessor;
39 import com.intellij.openapi.command.WriteCommandAction;
40 import com.intellij.openapi.diagnostic.Logger;
41 import com.intellij.openapi.editor.*;
42 import com.intellij.openapi.editor.event.*;
43 import com.intellij.openapi.keymap.KeymapUtil;
44 import com.intellij.openapi.project.Project;
45 import com.intellij.openapi.ui.popup.JBPopup;
46 import com.intellij.openapi.ui.popup.JBPopupFactory;
47 import com.intellij.openapi.util.Condition;
48 import com.intellij.openapi.util.Disposer;
49 import com.intellij.openapi.util.IconLoader;
50 import com.intellij.openapi.util.Trinity;
51 import com.intellij.openapi.util.registry.Registry;
52 import com.intellij.openapi.util.text.StringUtil;
53 import com.intellij.psi.PsiDocumentManager;
54 import com.intellij.psi.PsiElement;
55 import com.intellij.psi.PsiFile;
56 import com.intellij.psi.impl.DebugUtil;
57 import com.intellij.ui.LightweightHint;
58 import com.intellij.ui.ListScrollingUtil;
59 import com.intellij.ui.ScreenUtil;
60 import com.intellij.ui.awt.RelativePoint;
61 import com.intellij.ui.components.JBList;
62 import com.intellij.ui.components.JBScrollPane;
63 import com.intellij.ui.plaf.beg.BegPopupMenuBorder;
64 import com.intellij.ui.popup.AbstractPopup;
65 import com.intellij.util.Alarm;
66 import com.intellij.util.CollectConsumer;
67 import com.intellij.util.ObjectUtils;
68 import com.intellij.util.SmartList;
69 import com.intellij.util.containers.ConcurrentHashMap;
70 import com.intellij.util.containers.ContainerUtil;
71 import com.intellij.util.ui.AbstractLayoutManager;
72 import com.intellij.util.ui.AsyncProcessIcon;
73 import com.intellij.util.ui.ButtonlessScrollBarUI;
74 import com.intellij.util.ui.UIUtil;
75 import gnu.trove.TObjectHashingStrategy;
76 import org.jetbrains.annotations.NotNull;
77 import org.jetbrains.annotations.Nullable;
78 import org.jetbrains.annotations.TestOnly;
79
80 import javax.swing.*;
81 import javax.swing.border.Border;
82 import javax.swing.border.EmptyBorder;
83 import javax.swing.border.LineBorder;
84 import javax.swing.event.ListSelectionEvent;
85 import javax.swing.event.ListSelectionListener;
86 import java.awt.*;
87 import java.awt.event.MouseAdapter;
88 import java.awt.event.MouseEvent;
89 import java.util.*;
90 import java.util.List;
91
92 public class LookupImpl extends LightweightHint implements LookupEx, Disposable {
93   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupImpl");
94   private static final int MAX_PREFERRED_COUNT = 5;
95
96   private static final LookupItem EMPTY_LOOKUP_ITEM = LookupItem.fromString("preselect");
97   private static final Icon relevanceSortIcon = IconLoader.getIcon("/ide/lookupRelevance.png");
98   private static final Icon lexiSortIcon = IconLoader.getIcon("/ide/lookupAlphanumeric.png");
99
100   private final Project myProject;
101   private final Editor myEditor;
102
103   private int myPreferredItemsCount;
104   private String myInitialPrefix;
105
106   private boolean myStableStart;
107   private RangeMarker myLookupStartMarker;
108   private final JList myList = new JBList(new DefaultListModel());
109   private final LookupCellRenderer myCellRenderer;
110   private Boolean myPositionedAbove = null;
111
112   private final ArrayList<LookupListener> myListeners = new ArrayList<LookupListener>();
113
114   private long myStampShown = 0;
115   private boolean myShown = false;
116   private boolean myDisposed = false;
117   private boolean myHidden = false;
118   private final List<LookupElement> myFrozenItems = new ArrayList<LookupElement>();
119   private String mySelectionInvariant = null;
120   private boolean mySelectionTouched;
121   private boolean myFocused = true;
122   private String myAdditionalPrefix = "";
123   private final AsyncProcessIcon myProcessIcon = new AsyncProcessIcon("Completion progress");
124   private final JPanel myIconPanel = new JPanel(new BorderLayout());
125   private volatile boolean myCalculating;
126   private final Advertiser myAdComponent;
127   private volatile String myAdText;
128   private volatile int myLookupTextWidth = 50;
129   private boolean myChangeGuard;
130   private volatile LookupModel myModel = new LookupModel(EMPTY_LOOKUP_ITEM);
131   private LookupModel myPresentableModel = myModel;
132   @SuppressWarnings("unchecked") private final Map<LookupElement, PrefixMatcher> myMatchers = new ConcurrentHashMap<LookupElement, PrefixMatcher>(TObjectHashingStrategy.IDENTITY);
133   private LookupHint myElementHint = null;
134   private Alarm myHintAlarm = new Alarm();
135   private JLabel mySortingLabel = new JLabel();
136   private final JScrollPane myScrollPane;
137   private final LookupLayeredPane myLayeredPane = new LookupLayeredPane();
138   private JButton myScrollBarIncreaseButton;
139   private boolean myStartCompletionWhenNothingMatches;
140   private boolean myResizePending;
141   private int myMaximumHeight = Integer.MAX_VALUE;
142
143   public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger){
144     super(new JPanel(new BorderLayout()));
145     setForceShowAsPopup(true);
146     setCancelOnClickOutside(false);
147     setResizable(true);
148     AbstractPopup.suppressMacCornerFor(getComponent());
149
150     myProject = project;
151     myEditor = editor;
152
153     myIconPanel.setVisible(false);
154     myCellRenderer = new LookupCellRenderer(this);
155     myList.setCellRenderer(myCellRenderer);
156
157     myList.setFocusable(false);
158     myList.setFixedCellWidth(50);
159
160     myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
161     myList.setBackground(LookupCellRenderer.BACKGROUND_COLOR);
162
163     myScrollBarIncreaseButton = new JButton();
164     myScrollBarIncreaseButton.setFocusable(false);
165     myScrollBarIncreaseButton.setRequestFocusEnabled(false);
166
167     myScrollPane = new JBScrollPane(myList);
168     myScrollPane.setViewportBorder(new EmptyBorder(0, 0, 0, 0));
169     myScrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
170     myScrollPane.getVerticalScrollBar().setPreferredSize(new Dimension(13, -1));
171     myScrollPane.getVerticalScrollBar().setUI(new ButtonlessScrollBarUI() {
172       @Override
173       protected JButton createIncreaseButton(int orientation) {
174         return myScrollBarIncreaseButton;
175       }
176     });
177     
178     getComponent().add(myLayeredPane, BorderLayout.CENTER);
179
180     myLayeredPane.mainPanel.add(myScrollPane, BorderLayout.CENTER);
181     myScrollPane.setBorder(null);
182
183     myAdComponent = new Advertiser();
184     JComponent adComponent = myAdComponent.getAdComponent();
185     adComponent.setBorder(new EmptyBorder(0, 1, 1, 2 + relevanceSortIcon.getIconWidth()));
186     myLayeredPane.mainPanel.add(adComponent, BorderLayout.SOUTH);
187     getComponent().setBorder(new BegPopupMenuBorder());
188
189     myIconPanel.setBackground(Color.LIGHT_GRAY);
190     myIconPanel.add(myProcessIcon);
191
192     updateLookupStart(0);
193
194     final ListModel model = myList.getModel();
195     addEmptyItem((DefaultListModel)model);
196     updateListHeight(model);
197
198     setArranger(arranger);
199
200     addListeners();
201
202     mySortingLabel.setBorder(new LineBorder(Color.LIGHT_GRAY));
203     mySortingLabel.setOpaque(true);
204     mySortingLabel.addMouseListener(new MouseAdapter() {
205       @Override
206       public void mouseClicked(MouseEvent e) {
207         FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CHANGE_SORTING);
208         UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY = !UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
209         updateSorting();
210       }
211     });
212     updateSorting();
213   }
214
215   private void updateSorting() {
216     final boolean lexi = UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
217     mySortingLabel.setIcon(lexi ? lexiSortIcon : relevanceSortIcon);
218     mySortingLabel.setToolTipText(lexi ? "Click to sort variants by relevance" : "Click to sort variants alphabetically");
219
220     resort();
221   }
222
223   public void setArranger(LookupArranger arranger) {
224     myModel.setArranger(arranger);
225   }
226
227   @Override
228   public boolean isFocused() {
229     return myFocused;
230   }
231
232   public void setFocused(boolean focused) {
233     myFocused = focused;
234   }
235
236   public boolean isCalculating() {
237     return myCalculating;
238   }
239
240   public void setCalculating(final boolean calculating) {
241     myCalculating = calculating;
242     Runnable setVisible = new Runnable() {
243       @Override
244       public void run() {
245         myIconPanel.setVisible(myCalculating);
246       }
247     };
248     if (myCalculating) {
249       new Alarm().addRequest(setVisible, 100);
250     } else {
251       setVisible.run();
252     }
253
254     if (calculating) {
255       myProcessIcon.resume();
256     } else {
257       myProcessIcon.suspend();
258     }
259   }
260
261   public int getPreferredItemsCount() {
262     return myPreferredItemsCount;
263   }
264
265   public void markSelectionTouched() {
266     if (!ApplicationManager.getApplication().isUnitTestMode()) {
267       ApplicationManager.getApplication().assertIsDispatchThread();
268     }
269     mySelectionTouched = true;
270     myPresentableModel.preselectedItem = null;
271     myList.repaint();
272   }
273
274   @TestOnly
275   public void setSelectionTouched(boolean selectionTouched) {
276     mySelectionTouched = selectionTouched;
277   }
278
279   public void resort() {
280     myFrozenItems.clear();
281     
282     myPresentableModel.preselectedItem = EMPTY_LOOKUP_ITEM;
283     synchronized (myList) {
284       ((DefaultListModel)myList.getModel()).clear();
285     }
286
287     final List<LookupElement> items = myPresentableModel.getItems();
288     myPresentableModel.clearItems();
289     for (final LookupElement item : items) {
290       addItem(item, itemMatcher(item));
291     }
292     refreshUi(true);
293   }
294
295   public void addItem(LookupElement item, PrefixMatcher matcher) {
296     myMatchers.put(item, matcher);
297     myModel.addItem(item);
298
299     updateLookupWidth(item);
300   }
301
302   public void updateLookupWidth(LookupElement item) {
303     final LookupElementPresentation presentation = renderItemApproximately(item);
304     int maxWidth = myCellRenderer.updateMaximumWidth(presentation);
305     myLookupTextWidth = Math.max(maxWidth, myLookupTextWidth);
306
307     myModel.setItemPresentation(item, presentation);
308   }
309
310   public void requestResize() {
311     ApplicationManager.getApplication().assertIsDispatchThread();
312     myResizePending = true;
313   }
314
315   public Collection<LookupElementAction> getActionsFor(LookupElement element) {
316     final CollectConsumer<LookupElementAction> consumer = new CollectConsumer<LookupElementAction>();
317     for (LookupActionProvider provider : LookupActionProvider.EP_NAME.getExtensions()) {
318       provider.fillActions(element, this, consumer);
319     }
320     return consumer.getResult();
321   }
322
323   public JList getList() {
324     return myList;
325   }
326
327   public List<LookupElement> getItems() {
328     final ArrayList<LookupElement> result = new ArrayList<LookupElement>();
329     final Object[] objects;
330     synchronized (myList) {
331       objects = ((DefaultListModel)myList.getModel()).toArray();
332     }
333     for (final Object object : objects) {
334       if (!(object instanceof EmptyLookupItem)) {
335         result.add((LookupElement) object);
336       }
337     }
338     return result;
339   }
340
341   public void setAdvertisementText(@Nullable String text) {
342     myAdText = text;
343     if (StringUtil.isNotEmpty(text)) {
344       addAdvertisement(ObjectUtils.assertNotNull(text));
345     }
346   }
347
348   public String getAdvertisementText() {
349     return myAdText;
350   }
351
352
353   public String getAdditionalPrefix() {
354     return myAdditionalPrefix;
355   }
356
357   void appendPrefix(char c) {
358     checkValid();
359     myAdditionalPrefix += c;
360     myInitialPrefix = null;
361     myFrozenItems.clear();
362     requestResize();
363     refreshUi(false);
364     ensureSelectionVisible();
365   }
366
367   public void setStartCompletionWhenNothingMatches(boolean startCompletionWhenNothingMatches) {
368     myStartCompletionWhenNothingMatches = startCompletionWhenNothingMatches;
369   }
370
371   public boolean isStartCompletionWhenNothingMatches() {
372     return myStartCompletionWhenNothingMatches;
373   }
374
375   public void ensureSelectionVisible() {
376     if (!isSelectionVisible()) {
377       ListScrollingUtil.ensureIndexIsVisible(myList, myList.getSelectedIndex(), 1);
378     }
379   }
380
381   boolean truncatePrefix(boolean preserveSelection) {
382     final int len = myAdditionalPrefix.length();
383     if (len == 0) return false;
384
385     if (preserveSelection) {
386       markSelectionTouched();
387     }
388
389     myAdditionalPrefix = myAdditionalPrefix.substring(0, len - 1);
390     myInitialPrefix = null;
391     myFrozenItems.clear();
392     requestResize();
393     if (myPresentableModel == myModel) {
394       refreshUi(false);
395       ensureSelectionVisible();
396     }
397
398     return true;
399   }
400
401   private boolean updateList() {
402     if (!ApplicationManager.getApplication().isUnitTestMode()) {
403       ApplicationManager.getApplication().assertIsDispatchThread();
404     }
405     checkValid();
406
407     final Trinity<List<LookupElement>, Iterable<List<LookupElement>>, Boolean> snapshot = myPresentableModel.getModelSnapshot();
408
409     final LinkedHashSet<LookupElement> items = matchingItems(snapshot.first);
410
411     checkMinPrefixLengthChanges(items);
412
413     boolean hasPreselected = !mySelectionTouched && items.contains(myPresentableModel.preselectedItem);
414     LookupElement oldSelected = mySelectionTouched ? (LookupElement)myList.getSelectedValue() : null;
415     String oldInvariant = mySelectionInvariant;
416
417     LinkedHashSet<LookupElement> model = new LinkedHashSet<LookupElement>();
418     model.addAll(getPrefixItems(items, true));
419     model.addAll(getPrefixItems(items, false));
420
421     myFrozenItems.retainAll(items);
422     model.addAll(myFrozenItems);
423
424     if (!isAlphaSorted()) {
425       addMostRelevantItems(model, snapshot.second);
426       if (hasPreselected) {
427         model.add(myPresentableModel.preselectedItem);
428       }
429     }
430
431     myPreferredItemsCount = model.size();
432     myFrozenItems.clear();
433     if (myShown) {
434       myFrozenItems.addAll(model);
435     }
436
437     if (isAlphaSorted()) {
438       final ArrayList<LookupElement> elements = new ArrayList<LookupElement>(items);
439       Collections.sort(elements, new Comparator<LookupElement>() {
440         @Override
441         public int compare(LookupElement o1, LookupElement o2) {
442           return o1.getLookupString().compareToIgnoreCase(o2.getLookupString());
443         }
444       });
445       model.addAll(elements);
446     } else if (limitRelevance()) {
447       model.addAll(addRemainingItemsLexicographically(model, items));
448     } else  {
449       for (List<LookupElement> group : snapshot.second) {
450         for (LookupElement element : group) {
451           if (prefixMatches(element)) {
452             model.add(element);
453           }
454         }
455       }
456     }
457
458     DefaultListModel listModel = (DefaultListModel)myList.getModel();
459     synchronized (myList) {
460       listModel.clear();
461
462       if (!model.isEmpty()) {
463         for (LookupElement element : model) {
464           listModel.addElement(element);
465         }
466       }
467       else {
468         addEmptyItem(listModel);
469       }
470     }
471
472     updateListHeight(listModel);
473
474     if (!model.isEmpty()) {
475       LookupElement first = model.iterator().next();
476       if (isFocused() && (!isExactPrefixItem(first, true) || mySelectionTouched || shouldSkip(first))) {
477         restoreSelection(oldSelected, hasPreselected, oldInvariant, snapshot.second);
478       }
479       else {
480         myList.setSelectedIndex(0);
481       }
482     }
483     return snapshot.third;
484   }
485
486   private static boolean shouldSkip(LookupElement element) {
487     return element instanceof LiveTemplateLookupElement && ((LiveTemplateLookupElement)element).sudden;
488   }
489
490   private boolean isSelectionVisible() {
491     return myList.getFirstVisibleIndex() <= myList.getSelectedIndex() && myList.getSelectedIndex() <= myList.getLastVisibleIndex();
492   }
493
494   private LinkedHashSet<LookupElement> matchingItems(final List<LookupElement> elements) {
495     final LinkedHashSet<LookupElement> items = new LinkedHashSet<LookupElement>();
496     for (LookupElement element : elements) {
497       if (prefixMatches(element)) {
498         items.add(element);
499       }
500     }
501     return items;
502   }
503
504   private boolean checkReused() {
505     if (myPresentableModel != myModel) {
506       myAdditionalPrefix = "";
507       myFrozenItems.clear();
508       myPresentableModel = myModel;
509       return true;
510     }
511     return false;
512   }
513
514   private void checkMinPrefixLengthChanges(Collection<LookupElement> items) {
515     if (myStableStart) return;
516     if (!myCalculating && !items.isEmpty()) {
517       myStableStart = true;
518     }
519
520     int minPrefixLength = items.isEmpty() ? 0 : Integer.MAX_VALUE;
521     for (final LookupElement item : items) {
522       minPrefixLength = Math.min(itemMatcher(item).getPrefix().length(), minPrefixLength);
523     }
524
525     updateLookupStart(minPrefixLength);
526   }
527
528   private void restoreSelection(@Nullable LookupElement oldSelected, boolean choosePreselectedItem, @Nullable String oldInvariant, Iterable<List<LookupElement>> groups) {
529     if (oldSelected != null) {
530       if (oldSelected.isValid()) {
531         myList.setSelectedValue(oldSelected, false);
532         if (myList.getSelectedValue() == oldSelected) {
533           return;
534         }
535       }
536
537       if (oldInvariant != null) {
538         for (LookupElement element : getItems()) {
539           if (oldInvariant.equals(myPresentableModel.getItemPresentationInvariant(element))) {
540             myList.setSelectedValue(element, false);
541             if (myList.getSelectedValue() == element) {
542               return;
543             }
544           }
545         }
546       }
547     }
548
549     LookupElement preselected = myPresentableModel.preselectedItem;
550     if (choosePreselectedItem) {
551       myList.setSelectedValue(preselected, false);
552     } else {
553       myList.setSelectedIndex(doSelectMostPreferableItem(getItems(), groups));
554     }
555
556     if (preselected != null && myShown) {
557       myPresentableModel.preselectedItem = getCurrentItem();
558     }
559   }
560
561   private void updateListHeight(ListModel model) {
562     myList.setFixedCellHeight(myCellRenderer.getListCellRendererComponent(myList, model.getElementAt(0), 0, false, false).getPreferredSize().height);
563
564     myList.setVisibleRowCount(Math.min(model.getSize(), UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT));
565   }
566
567   private void addEmptyItem(DefaultListModel model) {
568     LookupItem<String> item = new EmptyLookupItem(myCalculating ? " " : LangBundle.message("completion.no.suggestions"));
569     myMatchers.put(item, new CamelHumpMatcher(""));
570     model.addElement(item);
571
572     updateLookupWidth(item);
573     requestResize();
574   }
575
576   private static LookupElementPresentation renderItemApproximately(LookupElement item) {
577     final LookupElementPresentation p = new LookupElementPresentation();
578     item.renderElement(p);
579     return p;
580   }
581
582   private static List<LookupElement> addRemainingItemsLexicographically(Set<LookupElement> firstItems, Collection<LookupElement> allItems) {
583     List<LookupElement> model = new ArrayList<LookupElement>();
584     for (LookupElement item : allItems) {
585       if (!firstItems.contains(item)) {
586         model.add(item);
587       }
588     }
589     return model;
590   }
591
592   private void addMostRelevantItems(final Set<LookupElement> model, final Iterable<List<LookupElement>> sortedItems) {
593     if (model.size() > MAX_PREFERRED_COUNT) return;
594
595     for (final List<LookupElement> elements : sortedItems) {
596       final List<LookupElement> suitable = new SmartList<LookupElement>();
597       for (final LookupElement item : elements) {
598         if (!model.contains(item) && prefixMatches(item)) {
599           suitable.add(item);
600         }
601       }
602
603       if (model.size() + suitable.size() > MAX_PREFERRED_COUNT) break;
604       model.addAll(suitable);
605     }
606   }
607
608   public static boolean limitRelevance() {
609     return Registry.is("limited.relevance.sorting.in.completion");
610   }
611
612   public boolean isFrozen(@NotNull LookupElement element) {
613     return myFrozenItems.contains(element);
614   }
615
616   private List<LookupElement> getPrefixItems(final Collection<LookupElement> elements, final boolean caseSensitive) {
617     List<LookupElement> better = new ArrayList<LookupElement>();
618     for (LookupElement element : elements) {
619       if (isExactPrefixItem(element, caseSensitive)) {
620         better.add(element);
621       }
622     }
623
624     final Comparator<LookupElement> itemComparator = myPresentableModel.getArranger().getItemComparator();
625     if (itemComparator != null) {
626       Collections.sort(better, itemComparator);
627     }
628
629     List<LookupElement> classified = myPresentableModel.classifyByRelevance(better);
630     List<LookupElement> result = new ArrayList<LookupElement>(classified.size());
631     for (LookupElement element : classified) {
632       if (element.getLookupString().equals(itemPattern(element))) {
633         result.add(element);
634       }
635     }
636     for (LookupElement element : classified) {
637       if (!element.getLookupString().equals(itemPattern(element))) {
638         result.add(element);
639       }
640     }
641     return result;
642   }
643
644   @NotNull
645   @Override
646   public String itemPattern(LookupElement element) {
647     return itemMatcher(element).getPrefix() + myAdditionalPrefix;
648   }
649
650   private boolean isAlphaSorted() {
651     return isCompletion() && UISettings.getInstance().SORT_LOOKUP_ELEMENTS_LEXICOGRAPHICALLY;
652   }
653
654   private boolean isExactPrefixItem(LookupElement item, final boolean caseSensitive) {
655     final String pattern = itemPattern(item);
656     final Set<String> strings = item.getAllLookupStrings();
657     if (strings.contains(pattern)) {
658       return caseSensitive; //to not add the same elements twice to the model, as sensitive and then as insensitive
659     }
660
661     if (caseSensitive) {
662       return false;
663     }
664
665     for (String s : strings) {
666       if (s.equalsIgnoreCase(pattern)) {
667         return true;
668       }
669     }
670     return false;
671   }
672
673   private boolean prefixMatches(final LookupElement item) {
674     PrefixMatcher matcher = itemMatcher(item);
675     if (myAdditionalPrefix.length() > 0) {
676       matcher = matcher.cloneWithPrefix(itemPattern(item));
677     }
678     return matcher.prefixMatches(item);
679   }
680
681   @Override
682   @NotNull
683   public PrefixMatcher itemMatcher(LookupElement item) {
684     PrefixMatcher matcher = myMatchers.get(item);
685     if (matcher == null) {
686       throw new AssertionError("Item not in lookup: item=" + item + "; lookup items=" + getItems());
687     }
688     return matcher;
689   }
690
691   // in layered pane coordinate system.
692   private Rectangle calculatePosition() {
693     Dimension dim = getComponent().getPreferredSize();
694     int lookupStart = getLookupStart();
695     if (lookupStart < 0 || lookupStart > myEditor.getDocument().getTextLength()) {
696       LOG.error(lookupStart + "; offset=" + myEditor.getCaretModel().getOffset() + "; element=" +
697                 getPsiElement());
698     }
699
700     LogicalPosition pos = myEditor.offsetToLogicalPosition(lookupStart);
701     Point location = myEditor.logicalPositionToXY(pos);
702     location.y += myEditor.getLineHeight();
703     location.x -= myCellRenderer.getIconIndent() + getComponent().getInsets().left;
704
705     SwingUtilities.convertPointToScreen(location, myEditor.getContentComponent());
706     final Rectangle screenRectangle = ScreenUtil.getScreenRectangle(location);
707
708     if (!isPositionedAboveCaret()) {
709       int shiftLow = screenRectangle.height - (location.y + dim.height);
710       myPositionedAbove = shiftLow < 0 && shiftLow < location.y - dim.height && location.y >= dim.height;
711     }
712     if (isPositionedAboveCaret()) {
713       location.y -= dim.height + myEditor.getLineHeight();
714       if (pos.line == 0) {
715         location.y += 1;
716         //otherwise the lookup won't intersect with the editor and every editor's resize (e.g. after typing in console) will close the lookup
717       }
718     }
719
720     if (!screenRectangle.contains(location)) {
721       location = ScreenUtil.findNearestPointOnBorder(screenRectangle, location);
722     }
723
724     final JRootPane rootPane = myEditor.getComponent().getRootPane();
725     if (rootPane == null) {
726       LOG.error(myEditor.isDisposed() + "; shown=" + myShown + "; disposed=" + myDisposed + "; editorShowing=" + myEditor.getContentComponent().isShowing());
727     }
728     Rectangle candidate = new Rectangle(location, dim);
729     ScreenUtil.cropRectangleToFitTheScreen(candidate);
730     
731     SwingUtilities.convertPointFromScreen(location, rootPane.getLayeredPane());
732     return new Rectangle(location.x, location.y, dim.width, candidate.height);
733   }
734
735   public void finishLookup(final char completionChar) {
736     finishLookup(completionChar, (LookupElement)myList.getSelectedValue());
737   }
738
739   public void finishLookup(char completionChar, @Nullable final LookupElement item) {
740     if (item == null ||
741         item instanceof EmptyLookupItem ||
742         item.getObject() instanceof DeferredUserLookupValue &&
743         item.as(LookupItem.CLASS_CONDITION_KEY) != null &&
744         !((DeferredUserLookupValue)item.getObject()).handleUserSelection(item.as(LookupItem.CLASS_CONDITION_KEY), myProject)) {
745       doHide(false, true);
746       fireItemSelected(null, completionChar);
747       return;
748     }
749
750     final PsiFile file = getPsiFile();
751     if (file != null && !WriteCommandAction.ensureFilesWritable(myProject, Arrays.asList(file))) {
752       doHide(false, true);
753       fireItemSelected(null, completionChar);
754       return;
755     }
756
757     final String prefix = itemPattern(item);
758     boolean plainMatch = ContainerUtil.or(item.getAllLookupStrings(), new Condition<String>() {
759       @Override
760       public boolean value(String s) {
761         return StringUtil.startsWithIgnoreCase(s, prefix);
762       }
763     });
764     if (!plainMatch) {
765       FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CAMEL_HUMPS);
766     }
767     
768     performGuardedChange(new Runnable() {
769       public void run() {
770         AccessToken token = WriteAction.start();
771         try {
772           insertLookupString(item, prefix);
773         }
774         finally {
775           token.finish();
776         }
777       }
778     });
779
780     doHide(false, true);
781
782     fireItemSelected(item, completionChar);
783   }
784
785   private void insertLookupString(LookupElement item, final String prefix) {
786     String lookupString = getCaseCorrectedLookupString(item);
787
788     if (myEditor.getSelectionModel().hasBlockSelection()) {
789       LogicalPosition blockStart = myEditor.getSelectionModel().getBlockStart();
790       LogicalPosition blockEnd = myEditor.getSelectionModel().getBlockEnd();
791       assert blockStart != null && blockEnd != null;
792
793       for (int line = blockStart.line; line <= blockEnd.line; line++) {
794         myEditor.getDocument().replaceString(myEditor.logicalPositionToOffset(new LogicalPosition(line, blockStart.column)) - prefix.length(),
795                                              myEditor.logicalPositionToOffset(new LogicalPosition(line, blockEnd.column)),
796                                              lookupString);
797       }
798       LogicalPosition start = new LogicalPosition(blockStart.line, blockStart.column - prefix.length());
799       LogicalPosition end = new LogicalPosition(blockEnd.line, start.column + lookupString.length());
800       myEditor.getSelectionModel().setBlockSelection(start, end);
801       myEditor.getCaretModel().moveToLogicalPosition(end);
802     } else {
803       EditorModificationUtil.deleteSelectedText(myEditor);
804       final int caretOffset = myEditor.getCaretModel().getOffset();
805       int lookupStart = caretOffset - prefix.length();
806   
807       int len = myEditor.getDocument().getTextLength();
808       LOG.assertTrue(lookupStart >= 0 && lookupStart <= len, "ls: " + lookupStart + " caret: " + caretOffset + " prefix:" + prefix + " doc: " + len);
809       LOG.assertTrue(caretOffset >= 0 && caretOffset <= len, "co: " + caretOffset + " doc: " + len);
810   
811       myEditor.getDocument().replaceString(lookupStart, caretOffset, lookupString);
812   
813       int offset = lookupStart + lookupString.length();
814       myEditor.getCaretModel().moveToOffset(offset);
815       myEditor.getSelectionModel().removeSelection();
816     }
817
818     myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
819   }
820
821   private String getCaseCorrectedLookupString(LookupElement item) {
822     String lookupString = item.getLookupString();
823     if (item.isCaseSensitive()) {
824       return lookupString;
825     }
826     
827     final String prefix = itemPattern(item);
828     final int length = prefix.length();
829     if (length == 0 || !StringUtil.startsWithIgnoreCase(lookupString, prefix)) return lookupString;
830     boolean isAllLower = true;
831     boolean isAllUpper = true;
832     boolean sameCase = true;
833     for (int i = 0; i < length && (isAllLower || isAllUpper || sameCase); i++) {
834       final char c = prefix.charAt(i);
835       boolean isLower = Character.isLowerCase(c);
836       boolean isUpper = Character.isUpperCase(c);
837       // do not take this kind of symbols into account ('_', '@', etc.)
838       if (!isLower && !isUpper) continue;
839       isAllLower = isAllLower && isLower;
840       isAllUpper = isAllUpper && isUpper;
841       sameCase = sameCase && isLower == Character.isLowerCase(lookupString.charAt(i));
842     }
843     if (sameCase) return lookupString;
844     if (isAllLower) return lookupString.toLowerCase();
845     if (isAllUpper) return lookupString.toUpperCase();
846     return lookupString;
847   }
848
849   public int getLookupStart() {
850     LOG.assertTrue(myLookupStartMarker.isValid(), disposeTrace);
851     return myLookupStartMarker.getStartOffset();
852   }
853
854   public void performGuardedChange(Runnable change) {
855     performGuardedChange(change, null);
856   }
857
858   public void performGuardedChange(Runnable change, @Nullable final String debug) {
859     checkValid();
860     assert myLookupStartMarker.isValid();
861     assert !myChangeGuard;
862
863     myChangeGuard = true;
864     final Document document = myEditor.getDocument();
865     RangeMarkerSpy spy = new RangeMarkerSpy(myLookupStartMarker) {
866       @Override
867       protected void invalidated(DocumentEvent e) {
868         LOG.error("Lookup start marker invalidated, say thanks to the " + e +
869                   ", doc=" + document +
870                   ", debug=" + debug);
871       }
872     };
873     document.addDocumentListener(spy);
874     try {
875       change.run();
876     }
877     finally {
878       document.removeDocumentListener(spy);
879       myChangeGuard = false;
880     }
881     checkValid();
882     LOG.assertTrue(myLookupStartMarker.isValid(), "invalid lookup start");
883     if (isVisible()) {
884       updateLookupLocation();
885     }
886     checkValid();
887   }
888
889   @Override
890   public boolean vetoesHiding() {
891     return myChangeGuard || myDisposed;
892   }
893
894   public boolean isAvailableToUser() {
895     if (ApplicationManager.getApplication().isUnitTestMode()) {
896       return myShown;
897     }
898     return isVisible();
899   }
900
901   public boolean isShown() {
902     if (!ApplicationManager.getApplication().isUnitTestMode()) {
903       ApplicationManager.getApplication().assertIsDispatchThread();
904     }
905     return myShown;
906   }
907
908   public boolean showLookup() {
909     ApplicationManager.getApplication().assertIsDispatchThread();
910     checkValid();
911     LOG.assertTrue(!myShown);
912     myShown = true;
913     myStampShown = System.currentTimeMillis();
914
915     if (ApplicationManager.getApplication().isUnitTestMode()) return true;
916
917     if (!myEditor.getContentComponent().isShowing()) {
918       hide();
919       return false;
920     }
921
922     myAdComponent.showRandomText();
923
924     getComponent().setBorder(null);
925     updateScrollbarVisibility();
926
927     Rectangle bounds = calculatePosition();
928     myMaximumHeight = bounds.height;
929     Point p = bounds.getLocation();
930     HintManagerImpl.getInstanceImpl().showEditorHint(this, myEditor, p, HintManager.HIDE_BY_ESCAPE | HintManager.UPDATE_BY_SCROLLING, 0, false,
931                                                      HintManagerImpl.createHintHint(myEditor, p, this, HintManager.UNDER).setAwtTooltip(false));
932
933     if (!isVisible()) {
934       hide();
935       return false;
936     }
937
938     LOG.assertTrue(myList.isShowing(), "!showing, disposed=" + myDisposed);
939
940     return true;
941   }
942
943   public boolean mayBeNoticed() {
944     return myStampShown > 0 && System.currentTimeMillis() - myStampShown > 300;
945   }
946
947   private void addListeners() {
948     myEditor.getDocument().addDocumentListener(new DocumentAdapter() {
949       public void documentChanged(DocumentEvent e) {
950         if (!myChangeGuard) {
951           hide();
952         }
953       }
954     }, this);
955
956     final CaretListener caretListener = new CaretListener() {
957       public void caretPositionChanged(CaretEvent e) {
958         if (!myChangeGuard) {
959           hide();
960         }
961       }
962     };
963     final SelectionListener selectionListener = new SelectionListener() {
964       public void selectionChanged(final SelectionEvent e) {
965         if (!myChangeGuard) {
966           hide();
967         }
968       }
969     };
970     final EditorMouseListener mouseListener = new EditorMouseAdapter() {
971       public void mouseClicked(EditorMouseEvent e){
972         e.consume();
973         hide();
974       }
975     };
976
977     myEditor.getCaretModel().addCaretListener(caretListener);
978     myEditor.getSelectionModel().addSelectionListener(selectionListener);
979     myEditor.addEditorMouseListener(mouseListener);
980     Disposer.register(this, new Disposable() {
981       @Override
982       public void dispose() {
983         myEditor.getCaretModel().removeCaretListener(caretListener);
984         myEditor.getSelectionModel().removeSelectionListener(selectionListener);
985         myEditor.removeEditorMouseListener(mouseListener);
986       }
987     });
988
989     myList.addListSelectionListener(new ListSelectionListener() {
990       private LookupElement oldItem = null;
991
992       public void valueChanged(ListSelectionEvent e){
993         myHintAlarm.cancelAllRequests();
994
995         final LookupElement item = getCurrentItem();
996         if (oldItem != item) {
997           mySelectionInvariant = item == null ? null : myPresentableModel.getItemPresentationInvariant(item);
998           fireCurrentItemChanged(item);
999         }
1000         if (item != null) {
1001           updateHint(item);
1002         }
1003         oldItem = item;
1004       }
1005     });
1006
1007     myList.addMouseListener(new MouseAdapter() {
1008       public void mouseClicked(MouseEvent e){
1009         setFocused(true);
1010         markSelectionTouched();
1011
1012         if (e.getClickCount() == 2){
1013           CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
1014             public void run() {
1015               finishLookup(NORMAL_SELECT_CHAR);
1016             }
1017           }, "", null);
1018         }
1019       }
1020     });
1021   }
1022
1023   private void updateHint(@NotNull final LookupElement item) {
1024     checkValid();
1025     if (myElementHint != null) {
1026       myLayeredPane.remove(myElementHint);
1027       myElementHint = null;
1028       final JRootPane rootPane = getComponent().getRootPane();
1029       if (rootPane != null) {
1030         rootPane.revalidate();
1031         rootPane.repaint();
1032       }
1033     }
1034     if (!isFocused()) {
1035       return;
1036     }
1037
1038     final Collection<LookupElementAction> actions = getActionsFor(item);
1039     if (!actions.isEmpty()) {
1040       myHintAlarm.addRequest(new Runnable() {
1041         @Override
1042         public void run() {
1043           assert !myDisposed;
1044           myElementHint = new LookupHint();
1045           myLayeredPane.add(myElementHint, 20, 0);
1046           myLayeredPane.layoutHint();
1047         }
1048       }, 500);
1049     }
1050   }
1051
1052   private int updateLookupStart(int myMinPrefixLength) {
1053     int offset = myEditor.getSelectionModel().hasSelection()
1054                  ? myEditor.getSelectionModel().getSelectionStart()
1055                  : myEditor.getCaretModel().getOffset();
1056     int start = Math.max(offset - myMinPrefixLength - myAdditionalPrefix.length(), 0);
1057     if (myLookupStartMarker != null) {
1058       if (myLookupStartMarker.isValid() && myLookupStartMarker.getStartOffset() == start && myLookupStartMarker.getEndOffset() == start) {
1059         return start;
1060       }
1061       myLookupStartMarker.dispose();
1062     }
1063     myLookupStartMarker = myEditor.getDocument().createRangeMarker(start, start);
1064     myLookupStartMarker.setGreedyToLeft(true);
1065     return start;
1066   }
1067
1068   @Nullable
1069   public LookupElement getCurrentItem(){
1070     LookupElement item = (LookupElement)myList.getSelectedValue();
1071     return item instanceof EmptyLookupItem ? null : item;
1072   }
1073
1074   public void setCurrentItem(LookupElement item){
1075     markSelectionTouched();
1076     myList.setSelectedValue(item, false);
1077   }
1078
1079   public void addLookupListener(LookupListener listener){
1080     myListeners.add(listener);
1081   }
1082
1083   public void removeLookupListener(LookupListener listener){
1084     myListeners.remove(listener);
1085   }
1086
1087   public Rectangle getCurrentItemBounds(){
1088     int index = myList.getSelectedIndex();
1089     if (index < 0) {
1090       LOG.error("No selected element, size=" + myList.getModel().getSize() + "; items" + getItems());
1091     }
1092     Rectangle itmBounds = myList.getCellBounds(index, index);
1093     if (itmBounds == null){
1094       LOG.error("No bounds for " + index + "; size=" + myList.getModel().getSize());
1095       return null;
1096     }
1097     Point layeredPanePoint=SwingUtilities.convertPoint(myList,itmBounds.x,itmBounds.y,getComponent());
1098     itmBounds.x = layeredPanePoint.x;
1099     itmBounds.y = layeredPanePoint.y;
1100     return itmBounds;
1101   }
1102
1103   public void fireItemSelected(@Nullable final LookupElement item, char completionChar){
1104     PsiDocumentManager.getInstance(myProject).commitAllDocuments();
1105
1106     if (!myListeners.isEmpty()){
1107       LookupEvent event = new LookupEvent(this, item, completionChar);
1108       LookupListener[] listeners = myListeners.toArray(new LookupListener[myListeners.size()]);
1109       for (LookupListener listener : listeners) {
1110         try {
1111           listener.itemSelected(event);
1112         }
1113         catch (Throwable e) {
1114           LOG.error(e);
1115         }
1116       }
1117     }
1118   }
1119
1120   private void fireLookupCanceled(final boolean explicitly) {
1121     if (!myListeners.isEmpty()){
1122       LookupEvent event = new LookupEvent(this, explicitly);
1123       LookupListener[] listeners = myListeners.toArray(new LookupListener[myListeners.size()]);
1124       for (LookupListener listener : listeners) {
1125         try {
1126           listener.lookupCanceled(event);
1127         }
1128         catch (Throwable e) {
1129           LOG.error(e);
1130         }
1131       }
1132     }
1133   }
1134
1135   private void fireCurrentItemChanged(LookupElement item){
1136     if (!myListeners.isEmpty()){
1137       LookupEvent event = new LookupEvent(this, item, (char)0);
1138       LookupListener[] listeners = myListeners.toArray(new LookupListener[myListeners.size()]);
1139       for (LookupListener listener : listeners) {
1140         listener.currentItemChanged(event);
1141       }
1142     }
1143   }
1144
1145   public boolean fillInCommonPrefix(boolean explicitlyInvoked) {
1146     if (explicitlyInvoked) {
1147       setFocused(true);
1148     }
1149
1150     if (explicitlyInvoked && myCalculating) return false;
1151     if (!explicitlyInvoked && mySelectionTouched) return false;
1152
1153     ListModel listModel = myList.getModel();
1154     if (listModel.getSize() <= 1) return false;
1155
1156     if (listModel.getSize() == 0) return false;
1157
1158     final LookupElement firstItem = (LookupElement)listModel.getElementAt(0);
1159     if (listModel.getSize() == 1 && firstItem instanceof EmptyLookupItem) return false;
1160
1161     final PrefixMatcher firstItemMatcher = itemMatcher(firstItem);
1162     final String oldPrefix = firstItemMatcher.getPrefix();
1163     final String presentPrefix = oldPrefix + myAdditionalPrefix;
1164     String commonPrefix = getCaseCorrectedLookupString(firstItem);
1165
1166     for (int i = 1; i < listModel.getSize(); i++) {
1167       LookupElement item = (LookupElement)listModel.getElementAt(i);
1168       if (!oldPrefix.equals(itemMatcher(item).getPrefix())) return false;
1169
1170       final String lookupString = getCaseCorrectedLookupString(item);
1171       final int length = Math.min(commonPrefix.length(), lookupString.length());
1172       if (length < commonPrefix.length()) {
1173         commonPrefix = commonPrefix.substring(0, length);
1174       }
1175
1176       for (int j = 0; j < length; j++) {
1177         if (commonPrefix.charAt(j) != lookupString.charAt(j)) {
1178           commonPrefix = lookupString.substring(0, j);
1179           break;
1180         }
1181       }
1182
1183       if (commonPrefix.length() == 0 || commonPrefix.length() < presentPrefix.length()) {
1184         return false;
1185       }
1186     }
1187
1188     if (commonPrefix.equals(presentPrefix)) {
1189       return false;
1190     }
1191
1192     for (int i = 0; i < listModel.getSize(); i++) {
1193       LookupElement item = (LookupElement)listModel.getElementAt(i);
1194       if (!itemMatcher(item).cloneWithPrefix(commonPrefix).prefixMatches(item)) {
1195         return false;
1196       }
1197     }
1198
1199     if (myAdditionalPrefix.length() == 0 && myInitialPrefix == null && !explicitlyInvoked) {
1200       myInitialPrefix = presentPrefix;
1201     }
1202     else {
1203       myInitialPrefix = null;
1204     }
1205
1206     replacePrefix(presentPrefix, commonPrefix);
1207     return true;
1208   }
1209
1210   public void replacePrefix(final String presentPrefix, final String newPrefix) {
1211     performGuardedChange(new Runnable() {
1212       public void run() {
1213         EditorModificationUtil.deleteSelectedText(myEditor);
1214         int offset = myEditor.getCaretModel().getOffset();
1215         final int start = offset - presentPrefix.length();
1216         myEditor.getDocument().replaceString(start, offset, newPrefix);
1217
1218         Map<LookupElement, PrefixMatcher> newItems = myPresentableModel.retainMatchingItems(newPrefix, LookupImpl.this);
1219         myMatchers.clear();
1220         myMatchers.putAll(newItems);
1221
1222         myAdditionalPrefix = "";
1223
1224         myEditor.getCaretModel().moveToOffset(start + newPrefix.length());
1225       }
1226     });
1227     refreshUi(true);
1228   }
1229
1230   @Nullable
1231   public PsiFile getPsiFile() {
1232     return PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
1233   }
1234
1235   public boolean isCompletion() {
1236     return myModel.getArranger() instanceof CompletionLookupArranger;
1237   }
1238
1239   public PsiElement getPsiElement() {
1240     PsiFile file = getPsiFile();
1241     if (file == null) return null;
1242
1243     int offset = getLookupStart();
1244     if (offset > 0) return file.findElementAt(offset - 1);
1245
1246     return file.findElementAt(0);
1247   }
1248
1249   public Editor getEditor() {
1250     return myEditor;
1251   }
1252
1253   @TestOnly
1254   public void setPositionedAbove(boolean positionedAbove) {
1255     myPositionedAbove = positionedAbove;
1256   }
1257
1258   public boolean isPositionedAboveCaret(){
1259     return myPositionedAbove != null && myPositionedAbove.booleanValue();
1260   }
1261
1262   public boolean isSelectionTouched() {
1263     return mySelectionTouched;
1264   }
1265
1266   public void hide(){
1267     hideLookup(true);
1268   }
1269
1270   public void hideLookup(boolean explicitly) {
1271     ApplicationManager.getApplication().assertIsDispatchThread();
1272
1273     if (myDisposed) return;
1274
1275     doHide(true, explicitly);
1276   }
1277
1278   private void doHide(final boolean fireCanceled, final boolean explicitly) {
1279     if (myChangeGuard) {
1280       LOG.error("Disposing under a change guard");
1281     }
1282
1283     if (myDisposed) {
1284       LOG.error(disposeTrace);
1285     }
1286     else {
1287       myHidden = true;
1288
1289       try {
1290         super.hide();
1291
1292         Disposer.dispose(this);
1293
1294         assert myDisposed;
1295       }
1296       catch (Throwable e) {
1297         LOG.error(e);
1298       }
1299     }
1300
1301     if (fireCanceled) {
1302       fireLookupCanceled(explicitly);
1303     }
1304   }
1305
1306   public void restorePrefix() {
1307     if (myInitialPrefix != null) {
1308       myEditor.getDocument().replaceString(getLookupStart(), myEditor.getCaretModel().getOffset(), myInitialPrefix);
1309     }
1310   }
1311
1312   private static String staticDisposeTrace = null;
1313   private String disposeTrace = null;
1314
1315   public static String getLastLookupDisposeTrace() {
1316     return staticDisposeTrace;
1317   }
1318
1319   public void dispose() {
1320     assert ApplicationManager.getApplication().isDispatchThread();
1321     assert myHidden;
1322     if (myDisposed) {
1323       LOG.error(disposeTrace);
1324       return;
1325     }
1326
1327     if (myLookupStartMarker != null) {
1328       myLookupStartMarker.dispose();
1329       myLookupStartMarker = null;
1330     }
1331     Disposer.dispose(myProcessIcon);
1332     Disposer.dispose(myHintAlarm);
1333
1334     myDisposed = true;
1335     disposeTrace = DebugUtil.currentStackTrace() + "\n============";
1336     //noinspection AssignmentToStaticFieldFromInstanceMethod
1337     staticDisposeTrace = disposeTrace;
1338   }
1339
1340   private int doSelectMostPreferableItem(List<LookupElement> items, Iterable<List<LookupElement>> groups) {
1341     if (items.isEmpty()) {
1342       return -1;
1343     }
1344
1345     if (items.size() == 1) {
1346       return 0;
1347     }
1348
1349     for (int i = 0; i < items.size(); i++) {
1350       LookupElement item = items.get(i);
1351       if (isExactPrefixItem(item, true) && !shouldSkip(item)) {
1352         return i;
1353       }
1354     }
1355
1356     final int index = myModel.getArranger().suggestPreselectedItem(items, groups);
1357     assert index >= 0 && index < items.size();
1358     return index;
1359   }
1360
1361   public void refreshUi(boolean mayCheckReused) {
1362     final boolean reused = mayCheckReused && checkReused();
1363
1364     boolean selectionVisible = isSelectionVisible();
1365
1366     boolean itemsChanged = updateList();
1367
1368     if (isVisible()) {
1369       LOG.assertTrue(!ApplicationManager.getApplication().isUnitTestMode());
1370
1371       if (myEditor.getComponent().getRootPane() == null) {
1372         LOG.error("Null root pane");
1373       }
1374
1375       updateScrollbarVisibility();
1376
1377       if (myResizePending || itemsChanged) {
1378         myMaximumHeight = Integer.MAX_VALUE;
1379       }
1380       Rectangle rectangle = calculatePosition();
1381       myMaximumHeight = rectangle.height;
1382       
1383       if (myResizePending || itemsChanged) {
1384         myResizePending = false;
1385         pack();
1386       }
1387       HintManagerImpl.updateLocation(this, myEditor, rectangle.getLocation());
1388
1389       if (reused || selectionVisible) {
1390         ensureSelectionVisible();
1391       }
1392     }
1393   }
1394
1395   private void updateLookupLocation() {
1396     Rectangle rectangle = calculatePosition();
1397     myMaximumHeight = rectangle.height;
1398     HintManagerImpl.updateLocation(this, myEditor, rectangle.getLocation());
1399   }
1400
1401   private void updateScrollbarVisibility() {
1402     boolean showSorting = isCompletion() && getList().getModel().getSize() >= 3;
1403     mySortingLabel.setVisible(showSorting);
1404     myScrollPane.setVerticalScrollBarPolicy(showSorting ? ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS : ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
1405   }
1406
1407   public void markReused() {
1408     myAdComponent.clearAdvertisements();
1409     myModel = new LookupModel(null);
1410     requestResize();
1411   }
1412
1413   public void addAdvertisement(@NotNull final String text) {
1414     UIUtil.invokeLaterIfNeeded(new Runnable() {
1415       @Override
1416       public void run() {
1417         if (!myDisposed) {
1418           myAdComponent.addAdvertisement(text);
1419           requestResize();
1420           refreshUi(false);
1421         }
1422       }
1423     });
1424   }
1425
1426   public boolean isLookupDisposed() {
1427     return myDisposed;
1428   }
1429
1430   public void checkValid() {
1431     if (myDisposed) {
1432       throw new AssertionError("Disposed at: " + disposeTrace);
1433     }
1434   }
1435
1436   @Override
1437   public void showItemPopup(JBPopup hint) {
1438     final Rectangle bounds = getCurrentItemBounds();
1439     hint.show(new RelativePoint(getComponent(), new Point(bounds.x + bounds.width, bounds.y)));
1440   }
1441
1442   @Override
1443   public boolean showElementActions() {
1444     if (!isVisible()) return false;
1445
1446     final LookupElement element = getCurrentItem();
1447     if (element == null) {
1448       return false;
1449     }
1450
1451     final Collection<LookupElementAction> actions = getActionsFor(element);
1452     if (actions.isEmpty()) {
1453       return false;
1454     }
1455
1456     showItemPopup(JBPopupFactory.getInstance().createListPopup(new LookupActionsStep(actions, this, element)));
1457     return true;
1458   }
1459
1460   private class LookupLayeredPane extends JLayeredPane {
1461     final JPanel mainPanel = new JPanel(new BorderLayout());
1462
1463     private LookupLayeredPane() {
1464       add(mainPanel, 0, 0);
1465       add(myIconPanel, 42, 0);
1466       add(mySortingLabel, 10, 0);
1467
1468       setLayout(new AbstractLayoutManager() {
1469         @Override
1470         public Dimension preferredLayoutSize(@Nullable Container parent) {
1471           int maxCellWidth = myLookupTextWidth + myCellRenderer.getIconIndent();
1472           int scrollBarWidth = myScrollPane.getPreferredSize().width - myScrollPane.getViewport().getPreferredSize().width;
1473           int listWidth = Math.min(scrollBarWidth + maxCellWidth, UISettings.getInstance().MAX_LOOKUP_WIDTH);
1474           int adWidth = myAdComponent.getAdComponent().getPreferredSize().width;
1475           return new Dimension(Math.max(listWidth, adWidth),
1476                                Math.min(mainPanel.getPreferredSize().height, myMaximumHeight));
1477         }
1478
1479         @Override
1480         public void layoutContainer(Container parent) {
1481           Dimension size = getSize();
1482           mainPanel.setSize(size);
1483           mainPanel.validate();
1484
1485           if (!myResizePending) {
1486             Dimension preferredSize = preferredLayoutSize(null);
1487             if (preferredSize.width != size.width) {
1488               UISettings.getInstance().MAX_LOOKUP_WIDTH = Math.max(300, size.width);
1489             }
1490
1491             int listHeight = myList.getLastVisibleIndex() - myList.getFirstVisibleIndex() + 1;
1492             if (listHeight != myList.getModel().getSize() && listHeight != myList.getVisibleRowCount() && preferredSize.height != size.height) {
1493               UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT = Math.max(5, listHeight);
1494             }
1495           }
1496
1497           myList.setFixedCellWidth(myScrollPane.getViewport().getWidth());
1498           layoutStatusIcons();
1499           layoutHint();
1500         }
1501       });
1502     }
1503
1504     private void layoutStatusIcons() {
1505       int adHeight = myAdComponent.getAdComponent().getPreferredSize().height;
1506       Dimension buttonSize = adHeight > 0 || !mySortingLabel.isVisible() ? new Dimension(0, 0) : new Dimension(relevanceSortIcon.getIconWidth(), relevanceSortIcon.getIconHeight());
1507       myScrollBarIncreaseButton.setPreferredSize(buttonSize);
1508       myScrollBarIncreaseButton.setMinimumSize(buttonSize);
1509       myScrollBarIncreaseButton.setMaximumSize(buttonSize);
1510       myScrollPane.getVerticalScrollBar().revalidate();
1511       myScrollPane.getVerticalScrollBar().repaint();
1512
1513       final Dimension iconSize = myProcessIcon.getPreferredSize();
1514       myIconPanel.setBounds(getWidth() - iconSize.width, 0, iconSize.width, iconSize.height);
1515
1516       final Dimension sortSize = mySortingLabel.getPreferredSize();
1517       final Point sbLocation = SwingUtilities.convertPoint(myScrollPane.getVerticalScrollBar(), 0, 0, myLayeredPane);
1518
1519       final int sortHeight = Math.max(adHeight, mySortingLabel.getPreferredSize().height);
1520       mySortingLabel.setBounds(sbLocation.x, getHeight() - sortHeight, sortSize.width, sortHeight);
1521     }
1522
1523     void layoutHint() {
1524       if (myElementHint != null && getCurrentItem() != null) {
1525         final Rectangle bounds = getCurrentItemBounds();
1526         myElementHint.setSize(myElementHint.getPreferredSize());
1527         myElementHint.setLocation(new Point(bounds.x + bounds.width - myElementHint.getWidth(), bounds.y));
1528       }
1529     }
1530
1531   }
1532
1533   private class LookupHint extends JLabel {
1534     private final Border INACTIVE_BORDER = BorderFactory.createEmptyBorder(4, 4, 4, 4);
1535     private final Border ACTIVE_BORDER = BorderFactory.createCompoundBorder(BorderFactory.createLineBorder(Color.BLACK, 1), BorderFactory.createEmptyBorder(3, 3, 3, 3));
1536     private LookupHint() {
1537       setOpaque(false);
1538       setBorder(INACTIVE_BORDER);
1539       setIcon(IconLoader.findIcon("/actions/intentionBulb.png"));
1540       String acceleratorsText = KeymapUtil.getFirstKeyboardShortcutText(
1541               ActionManager.getInstance().getAction(IdeActions.ACTION_SHOW_INTENTION_ACTIONS));
1542       if (acceleratorsText.length() > 0) {
1543         setToolTipText(CodeInsightBundle.message("lightbulb.tooltip", acceleratorsText));
1544       }
1545
1546       addMouseListener(new MouseAdapter() {
1547         @Override
1548         public void mouseEntered(MouseEvent e) {
1549           setBorder(ACTIVE_BORDER);
1550         }
1551
1552         @Override
1553         public void mouseExited(MouseEvent e) {
1554           setBorder(INACTIVE_BORDER);
1555         }
1556         @Override
1557         public void mousePressed(MouseEvent e) {
1558           if (!e.isPopupTrigger() && e.getButton() == MouseEvent.BUTTON1) {
1559             showElementActions();
1560           }
1561         }
1562       });
1563     }
1564   }
1565
1566   public LinkedHashMap<LookupElement,StringBuilder> getRelevanceStrings() {
1567     return myPresentableModel.getRelevanceStrings();
1568   }
1569
1570 }