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