4d1908be9ffe8069890fc6512b4cdfe1b1bf1e33
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / lookup / impl / LookupImpl.java
1 /*
2  * Copyright 2000-2015 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.FileModificationService;
20 import com.intellij.codeInsight.completion.*;
21 import com.intellij.codeInsight.completion.impl.CamelHumpMatcher;
22 import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
23 import com.intellij.codeInsight.hint.HintManager;
24 import com.intellij.codeInsight.hint.HintManagerImpl;
25 import com.intellij.codeInsight.lookup.*;
26 import com.intellij.featureStatistics.FeatureUsageTracker;
27 import com.intellij.ide.IdeEventQueue;
28 import com.intellij.ide.ui.UISettings;
29 import com.intellij.lang.LangBundle;
30 import com.intellij.openapi.Disposable;
31 import com.intellij.openapi.application.ApplicationManager;
32 import com.intellij.openapi.command.CommandProcessor;
33 import com.intellij.openapi.diagnostic.Logger;
34 import com.intellij.openapi.editor.*;
35 import com.intellij.openapi.editor.event.*;
36 import com.intellij.openapi.editor.event.DocumentAdapter;
37 import com.intellij.openapi.project.Project;
38 import com.intellij.openapi.ui.popup.JBPopup;
39 import com.intellij.openapi.ui.popup.JBPopupFactory;
40 import com.intellij.openapi.util.Condition;
41 import com.intellij.openapi.util.Disposer;
42 import com.intellij.openapi.util.Pair;
43 import com.intellij.openapi.util.text.StringUtil;
44 import com.intellij.openapi.wm.IdeFocusManager;
45 import com.intellij.psi.PsiDocumentManager;
46 import com.intellij.psi.PsiElement;
47 import com.intellij.psi.PsiFile;
48 import com.intellij.psi.impl.DebugUtil;
49 import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
50 import com.intellij.ui.*;
51 import com.intellij.ui.awt.RelativePoint;
52 import com.intellij.ui.components.JBList;
53 import com.intellij.ui.popup.AbstractPopup;
54 import com.intellij.util.CollectConsumer;
55 import com.intellij.util.containers.ContainerUtil;
56 import com.intellij.util.ui.update.Activatable;
57 import com.intellij.util.ui.update.UiNotifyConnector;
58 import org.jetbrains.annotations.NotNull;
59 import org.jetbrains.annotations.Nullable;
60 import org.jetbrains.annotations.TestOnly;
61
62 import javax.swing.*;
63 import javax.swing.event.ListSelectionEvent;
64 import javax.swing.event.ListSelectionListener;
65 import java.awt.*;
66 import java.awt.event.KeyEvent;
67 import java.awt.event.MouseEvent;
68 import java.util.Collection;
69 import java.util.HashMap;
70 import java.util.List;
71 import java.util.Map;
72
73 public class LookupImpl extends LightweightHint implements LookupEx, Disposable, WeighingContext {
74   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupImpl");
75
76   private final LookupOffsets myOffsets;
77   private final Project myProject;
78   private final Editor myEditor;
79   private final JBList myList = new JBList(new CollectionListModel<LookupElement>()) {
80     @Override
81     protected void processKeyEvent(@NotNull final KeyEvent e) {
82       final char keyChar = e.getKeyChar();
83       if (keyChar == KeyEvent.VK_ENTER || keyChar == KeyEvent.VK_TAB) {
84         IdeFocusManager.getInstance(myProject).requestFocus(myEditor.getContentComponent(), true).doWhenDone(new Runnable() {
85           @Override
86           public void run() {
87             IdeEventQueue.getInstance().getKeyEventDispatcher().dispatchKeyEvent(e);
88           }
89         });
90         return;
91       }
92
93       super.processKeyEvent(e);
94     }
95
96     @NotNull
97     @Override
98     protected ExpandableItemsHandler<Integer> createExpandableItemsHandler() {
99       return new CompletionExtender(this);
100     }
101   };
102   final LookupCellRenderer myCellRenderer;
103
104   private final List<LookupListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
105
106   private long myStampShown = 0;
107   private boolean myShown = false;
108   private boolean myDisposed = false;
109   private boolean myHidden = false;
110   private boolean mySelectionTouched;
111   private FocusDegree myFocusDegree = FocusDegree.FOCUSED;
112   private volatile boolean myCalculating;
113   private final Advertiser myAdComponent;
114   volatile int myLookupTextWidth = 50;
115   private boolean myChangeGuard;
116   private volatile LookupArranger myArranger;
117   private LookupArranger myPresentableArranger;
118   private final Map<LookupElement, PrefixMatcher> myMatchers =
119     ContainerUtil.createConcurrentWeakMap(ContainerUtil.<LookupElement>identityStrategy());
120   private final Map<LookupElement, Font> myCustomFonts = ContainerUtil.createConcurrentWeakMap(10, 0.75f, Runtime.getRuntime().availableProcessors(),
121     ContainerUtil.<LookupElement>identityStrategy());
122   private boolean myStartCompletionWhenNothingMatches;
123   boolean myResizePending;
124   private boolean myFinishing;
125   boolean myUpdating;
126   private LookupUi myUi;
127
128   public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger) {
129     super(new JPanel(new BorderLayout()));
130     setForceShowAsPopup(true);
131     setCancelOnClickOutside(false);
132     setResizable(true);
133     AbstractPopup.suppressMacCornerFor(getComponent());
134
135     myProject = project;
136     myEditor = editor;
137     myArranger = arranger;
138     myPresentableArranger = arranger;
139
140     DaemonCodeAnalyzer.getInstance(myProject).disableUpdateByTimer(this);
141
142     myCellRenderer = new LookupCellRenderer(this);
143     myList.setCellRenderer(myCellRenderer);
144
145     myList.setFocusable(false);
146     myList.setFixedCellWidth(50);
147
148     myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
149     myList.setBackground(LookupCellRenderer.BACKGROUND_COLOR);
150
151     myList.getExpandableItemsHandler();
152
153     myAdComponent = new Advertiser();
154
155     myOffsets = new LookupOffsets(editor);
156
157     final CollectionListModel<LookupElement> model = getListModel();
158     addEmptyItem(model);
159     updateListHeight(model);
160
161     addListeners();
162   }
163
164   private CollectionListModel<LookupElement> getListModel() {
165     //noinspection unchecked
166     return (CollectionListModel<LookupElement>)myList.getModel();
167   }
168
169   public void setArranger(LookupArranger arranger) {
170     myArranger = arranger;
171   }
172
173   public FocusDegree getFocusDegree() {
174     return myFocusDegree;
175   }
176
177   @Override
178   public boolean isFocused() {
179     return getFocusDegree() == FocusDegree.FOCUSED;
180   }
181
182   public void setFocusDegree(FocusDegree focusDegree) {
183     myFocusDegree = focusDegree;
184   }
185
186   public boolean isCalculating() {
187     return myCalculating;
188   }
189
190   public void setCalculating(final boolean calculating) {
191     myCalculating = calculating;
192     if (myUi != null) {
193       myUi.setCalculating(calculating);
194     }
195   }
196
197   public void markSelectionTouched() {
198     if (!ApplicationManager.getApplication().isUnitTestMode()) {
199       ApplicationManager.getApplication().assertIsDispatchThread();
200     }
201     mySelectionTouched = true;
202     myList.repaint();
203   }
204
205   @TestOnly
206   public void setSelectionTouched(boolean selectionTouched) {
207     mySelectionTouched = selectionTouched;
208   }
209
210   public void resort(boolean addAgain) {
211     final List<LookupElement> items = getItems();
212
213     synchronized (myList) {
214       myPresentableArranger.prefixChanged(this);
215       getListModel().removeAll();
216     }
217
218     if (addAgain) {
219       for (final LookupElement item : items) {
220         addItem(item, itemMatcher(item));
221       }
222     }
223     refreshUi(true, true);
224   }
225
226   public boolean addItem(LookupElement item, PrefixMatcher matcher) {
227     LookupElementPresentation presentation = renderItemApproximately(item);
228     if (containsDummyIdentifier(presentation.getItemText()) ||
229         containsDummyIdentifier(presentation.getTailText()) ||
230         containsDummyIdentifier(presentation.getTypeText())) {
231       return false;
232     }
233
234     myMatchers.put(item, matcher);
235     updateLookupWidth(item, presentation);
236     synchronized (myList) {
237       myArranger.addElement(this, item, presentation);
238     }
239     return true;
240   }
241
242   private static boolean containsDummyIdentifier(@Nullable final String s) {
243     return s != null && s.contains(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED);
244   }
245
246   public void updateLookupWidth(LookupElement item) {
247     updateLookupWidth(item, renderItemApproximately(item));
248   }
249
250   private void updateLookupWidth(LookupElement item, LookupElementPresentation presentation) {
251     final Font customFont = myCellRenderer.getFontAbleToDisplay(presentation);
252     if (customFont != null) {
253       myCustomFonts.put(item, customFont);
254     }
255     int maxWidth = myCellRenderer.updateMaximumWidth(presentation, item);
256     myLookupTextWidth = Math.max(maxWidth, myLookupTextWidth);
257   }
258
259   @Nullable
260   public Font getCustomFont(LookupElement item, boolean bold) {
261     Font font = myCustomFonts.get(item);
262     return font == null ? null : bold ? font.deriveFont(Font.BOLD) : font;
263   }
264
265   public void requestResize() {
266     ApplicationManager.getApplication().assertIsDispatchThread();
267     myResizePending = true;
268   }
269
270   public Collection<LookupElementAction> getActionsFor(LookupElement element) {
271     final CollectConsumer<LookupElementAction> consumer = new CollectConsumer<LookupElementAction>();
272     for (LookupActionProvider provider : LookupActionProvider.EP_NAME.getExtensions()) {
273       provider.fillActions(element, this, consumer);
274     }
275     if (!consumer.getResult().isEmpty()) {
276       consumer.consume(new ShowHideIntentionIconLookupAction());
277     }
278     return consumer.getResult();
279   }
280
281   public JList getList() {
282     return myList;
283   }
284
285   @Override
286   public List<LookupElement> getItems() {
287     synchronized (myList) {
288       return ContainerUtil.findAll(getListModel().toList(), new Condition<LookupElement>() {
289         @Override
290         public boolean value(LookupElement element) {
291           return !(element instanceof EmptyLookupItem);
292         }
293       });
294     }
295   }
296
297   public String getAdditionalPrefix() {
298     return myOffsets.getAdditionalPrefix();
299   }
300
301   void appendPrefix(char c) {
302     checkValid();
303     myOffsets.appendPrefix(c);
304     synchronized (myList) {
305       myPresentableArranger.prefixChanged(this);
306     }
307     requestResize();
308     refreshUi(false, true);
309     ensureSelectionVisible(true);
310   }
311
312   public void setStartCompletionWhenNothingMatches(boolean startCompletionWhenNothingMatches) {
313     myStartCompletionWhenNothingMatches = startCompletionWhenNothingMatches;
314   }
315
316   public boolean isStartCompletionWhenNothingMatches() {
317     return myStartCompletionWhenNothingMatches;
318   }
319
320   public void ensureSelectionVisible(boolean forceTopSelection) {
321     if (isSelectionVisible() && !forceTopSelection) {
322       return;
323     }
324
325     if (!forceTopSelection) {
326       ScrollingUtil.ensureIndexIsVisible(myList, myList.getSelectedIndex(), 1);
327       return;
328     }
329
330     // selected item should be at the top of the visible list
331     int top = myList.getSelectedIndex();
332     if (top > 0) {
333       top--; // show one element above the selected one to give the hint that there are more available via scrolling
334     }
335
336     int firstVisibleIndex = myList.getFirstVisibleIndex();
337     if (firstVisibleIndex == top) {
338       return;
339     }
340
341     ScrollingUtil.ensureRangeIsVisible(myList, top, top + myList.getLastVisibleIndex() - firstVisibleIndex);
342   }
343
344   boolean truncatePrefix(boolean preserveSelection) {
345     if (!myOffsets.truncatePrefix()) {
346       return false;
347     }
348
349     if (preserveSelection) {
350       markSelectionTouched();
351     }
352
353     boolean shouldUpdate;
354     synchronized (myList) {
355       shouldUpdate = myPresentableArranger == myArranger;
356       myPresentableArranger.prefixChanged(this);
357     }
358     requestResize();
359     if (shouldUpdate) {
360       refreshUi(false, true);
361       ensureSelectionVisible(true);
362     }
363
364     return true;
365   }
366
367   private boolean updateList(boolean onExplicitAction, boolean reused) {
368     if (!ApplicationManager.getApplication().isUnitTestMode()) {
369       ApplicationManager.getApplication().assertIsDispatchThread();
370     }
371     checkValid();
372
373     CollectionListModel<LookupElement> listModel = getListModel();
374
375     Pair<List<LookupElement>, Integer> pair;
376     synchronized (myList) {
377       pair = myPresentableArranger.arrangeItems(this, onExplicitAction || reused);
378     }
379     
380     List<LookupElement> items = pair.first;
381     Integer toSelect = pair.second;
382     if (toSelect == null || toSelect < 0 || items.size() > 0 && toSelect >= items.size()) {
383       LOG.error("Arranger " + myPresentableArranger + " returned invalid selection index=" + toSelect + "; items=" + items);
384       toSelect = 0;
385     }
386
387     myOffsets.checkMinPrefixLengthChanges(items, this);
388     List<LookupElement> oldModel = listModel.toList();
389
390     listModel.removeAll();
391     if (!items.isEmpty()) {
392       listModel.add(items);
393     }
394     else {
395       addEmptyItem(listModel);
396     }
397
398     updateListHeight(listModel);
399
400     myList.setSelectedIndex(toSelect);
401     return !ContainerUtil.equalsIdentity(oldModel, items);
402   }
403
404   private boolean isSelectionVisible() {
405     return ScrollingUtil.isIndexFullyVisible(myList, myList.getSelectedIndex());
406   }
407
408   private boolean checkReused() {
409     synchronized (myList) {
410       if (myPresentableArranger != myArranger) {
411         myPresentableArranger = myArranger;
412         myOffsets.clearAdditionalPrefix();
413         myPresentableArranger.prefixChanged(this);
414         return true;
415       }
416       return false;
417     }
418   }
419
420   private void updateListHeight(ListModel model) {
421     myList.setFixedCellHeight(myCellRenderer.getListCellRendererComponent(myList, model.getElementAt(0), 0, false, false).getPreferredSize().height);
422
423     myList.setVisibleRowCount(Math.min(model.getSize(), UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT));
424   }
425
426   private void addEmptyItem(CollectionListModel<LookupElement> model) {
427     LookupElement item = new EmptyLookupItem(myCalculating ? " " : LangBundle.message("completion.no.suggestions"), false);
428     myMatchers.put(item, new CamelHumpMatcher(""));
429     model.add(item);
430
431     updateLookupWidth(item);
432     requestResize();
433   }
434
435   private static LookupElementPresentation renderItemApproximately(LookupElement item) {
436     final LookupElementPresentation p = new LookupElementPresentation();
437     item.renderElement(p);
438     return p;
439   }
440
441   @NotNull
442   @Override
443   public String itemPattern(@NotNull LookupElement element) {
444     String prefix = itemMatcher(element).getPrefix();
445     String additionalPrefix = getAdditionalPrefix();
446     return additionalPrefix.isEmpty() ? prefix : prefix + additionalPrefix;
447   }
448
449   @Override
450   @NotNull
451   public PrefixMatcher itemMatcher(@NotNull LookupElement item) {
452     PrefixMatcher matcher = itemMatcherNullable(item);
453     if (matcher == null) {
454       throw new AssertionError("Item not in lookup: item=" + item + "; lookup items=" + getItems());
455     }
456     return matcher;
457   }
458
459   public PrefixMatcher itemMatcherNullable(LookupElement item) {
460     return myMatchers.get(item);
461   }
462
463   public void finishLookup(final char completionChar) {
464     finishLookup(completionChar, (LookupElement)myList.getSelectedValue());
465   }
466
467   public void finishLookup(char completionChar, @Nullable final LookupElement item) {
468     //noinspection deprecation,unchecked
469     if (item == null ||
470         item instanceof EmptyLookupItem ||
471         item.getObject() instanceof DeferredUserLookupValue &&
472         item.as(LookupItem.CLASS_CONDITION_KEY) != null &&
473         !((DeferredUserLookupValue)item.getObject()).handleUserSelection(item.as(LookupItem.CLASS_CONDITION_KEY), myProject)) {
474       doHide(false, true);
475       fireItemSelected(null, completionChar);
476       return;
477     }
478
479     if (myDisposed) { // DeferredUserLookupValue could close us in any way
480       return;
481     }
482
483     final PsiFile file = getPsiFile();
484     boolean writableOk = file == null || FileModificationService.getInstance().prepareFileForWrite(file);
485     if (myDisposed) { // ensureFilesWritable could close us by showing a dialog
486       return;
487     }
488
489     if (!writableOk) {
490       doHide(false, true);
491       fireItemSelected(null, completionChar);
492       return;
493     }
494
495     final String prefix = itemPattern(item);
496     boolean plainMatch = ContainerUtil.or(item.getAllLookupStrings(), new Condition<String>() {
497       @Override
498       public boolean value(String s) {
499         return StringUtil.containsIgnoreCase(s, prefix);
500       }
501     });
502     if (!plainMatch) {
503       FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CAMEL_HUMPS);
504     }
505
506     myFinishing = true;
507     ApplicationManager.getApplication().runWriteAction(new Runnable() {
508       public void run() {
509         myEditor.getDocument().startGuardedBlockChecking();
510         try {
511           insertLookupString(item, getPrefixLength(item));
512         }
513         finally {
514           myEditor.getDocument().stopGuardedBlockChecking();
515         }
516       }
517     });
518
519     if (myDisposed) { // any document listeners could close us
520       return;
521     }
522
523     doHide(false, true);
524
525     fireItemSelected(item, completionChar);
526   }
527
528   public int getPrefixLength(LookupElement item) {
529     return myOffsets.getPrefixLength(item, this);
530   }
531
532   private void insertLookupString(LookupElement item, final int prefix) {
533     final String lookupString = getCaseCorrectedLookupString(item);
534
535     final Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor);
536     hostEditor.getCaretModel().runForEachCaret(new CaretAction() {
537       @Override
538       public void perform(Caret caret) {
539         EditorModificationUtil.deleteSelectedText(hostEditor);
540         final int caretOffset = hostEditor.getCaretModel().getOffset();
541         int lookupStart = Math.min(caretOffset, Math.max(caretOffset - prefix, 0));
542
543         int len = hostEditor.getDocument().getTextLength();
544         LOG.assertTrue(lookupStart >= 0 && lookupStart <= len,
545                        "ls: " + lookupStart + " caret: " + caretOffset + " prefix:" + prefix + " doc: " + len);
546         LOG.assertTrue(caretOffset >= 0 && caretOffset <= len, "co: " + caretOffset + " doc: " + len);
547
548         hostEditor.getDocument().replaceString(lookupStart, caretOffset, lookupString);
549
550         int offset = lookupStart + lookupString.length();
551         hostEditor.getCaretModel().moveToOffset(offset);
552         hostEditor.getSelectionModel().removeSelection();
553       }
554     });
555
556     myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
557   }
558
559   private String getCaseCorrectedLookupString(LookupElement item) {
560     String lookupString = item.getLookupString();
561     if (item.isCaseSensitive()) {
562       return lookupString;
563     }
564
565     final String prefix = itemPattern(item);
566     final int length = prefix.length();
567     if (length == 0 || !itemMatcher(item).prefixMatches(prefix)) return lookupString;
568     boolean isAllLower = true;
569     boolean isAllUpper = true;
570     boolean sameCase = true;
571     for (int i = 0; i < length && (isAllLower || isAllUpper || sameCase); i++) {
572       final char c = prefix.charAt(i);
573       boolean isLower = Character.isLowerCase(c);
574       boolean isUpper = Character.isUpperCase(c);
575       // do not take this kind of symbols into account ('_', '@', etc.)
576       if (!isLower && !isUpper) continue;
577       isAllLower = isAllLower && isLower;
578       isAllUpper = isAllUpper && isUpper;
579       sameCase = sameCase && isLower == Character.isLowerCase(lookupString.charAt(i));
580     }
581     if (sameCase) return lookupString;
582     if (isAllLower) return lookupString.toLowerCase();
583     if (isAllUpper) return StringUtil.toUpperCase(lookupString);
584     return lookupString;
585   }
586
587   @Override
588   public int getLookupStart() {
589     return myOffsets.getLookupStart(disposeTrace);
590   }
591
592   public int getLookupOriginalStart() {
593     return myOffsets.getLookupOriginalStart();
594   }
595
596   public boolean performGuardedChange(Runnable change) {
597     checkValid();
598     assert !myChangeGuard : "already in change";
599
600     myEditor.getDocument().startGuardedBlockChecking();
601     myChangeGuard = true;
602     boolean result;
603     try {
604       result = myOffsets.performGuardedChange(change);
605     }
606     finally {
607       myEditor.getDocument().stopGuardedBlockChecking();
608       myChangeGuard = false;
609     }
610     if (!result || myDisposed) {
611       hideLookup(false);
612       return false;
613     }
614     if (isVisible()) {
615       HintManagerImpl.updateLocation(this, myEditor, myUi.calculatePosition().getLocation());
616     }
617     checkValid();
618     return true;
619   }
620
621   @Override
622   public boolean vetoesHiding() {
623     return myChangeGuard;
624   }
625
626   public boolean isAvailableToUser() {
627     if (ApplicationManager.getApplication().isUnitTestMode()) {
628       return myShown;
629     }
630     return isVisible();
631   }
632
633   public boolean isShown() {
634     if (!ApplicationManager.getApplication().isUnitTestMode()) {
635       ApplicationManager.getApplication().assertIsDispatchThread();
636     }
637     return myShown;
638   }
639
640   public boolean showLookup() {
641     ApplicationManager.getApplication().assertIsDispatchThread();
642     checkValid();
643     LOG.assertTrue(!myShown);
644     myShown = true;
645     myStampShown = System.currentTimeMillis();
646
647     if (ApplicationManager.getApplication().isUnitTestMode()) return true;
648
649     if (!myEditor.getContentComponent().isShowing()) {
650       hideLookup(false);
651       return false;
652     }
653
654     myAdComponent.showRandomText();
655
656     myUi = new LookupUi(this, myAdComponent, myList, myProject);
657     myUi.setCalculating(myCalculating);
658     Point p = myUi.calculatePosition().getLocation();
659     try {
660       HintManagerImpl.getInstanceImpl().showEditorHint(this, myEditor, p, HintManager.HIDE_BY_ESCAPE | HintManager.UPDATE_BY_SCROLLING, 0, false,
661                                                        HintManagerImpl.createHintHint(myEditor, p, this, HintManager.UNDER).setAwtTooltip(false));
662     }
663     catch (Exception e) {
664       LOG.error(e);
665     }
666
667     if (!isVisible() || !myList.isShowing()) {
668       hideLookup(false);
669       return false;
670     }
671
672     return true;
673   }
674
675   public Advertiser getAdvertiser() {
676     return myAdComponent;
677   }
678
679   public boolean mayBeNoticed() {
680     return myStampShown > 0 && System.currentTimeMillis() - myStampShown > 300;
681   }
682
683   private void addListeners() {
684     myEditor.getDocument().addDocumentListener(new DocumentAdapter() {
685       @Override
686       public void documentChanged(DocumentEvent e) {
687         if (!myChangeGuard && !myFinishing) {
688           hideLookup(false);
689         }
690       }
691     }, this);
692
693     final CaretListener caretListener = new CaretAdapter() {
694       @Override
695       public void caretPositionChanged(CaretEvent e) {
696         if (!myChangeGuard && !myFinishing) {
697           hideLookup(false);
698         }
699       }
700     };
701     final SelectionListener selectionListener = new SelectionListener() {
702       @Override
703       public void selectionChanged(final SelectionEvent e) {
704         if (!myChangeGuard && !myFinishing) {
705           hideLookup(false);
706         }
707       }
708     };
709     final EditorMouseListener mouseListener = new EditorMouseAdapter() {
710       @Override
711       public void mouseClicked(EditorMouseEvent e){
712         e.consume();
713         hideLookup(false);
714       }
715     };
716
717     myEditor.getCaretModel().addCaretListener(caretListener);
718     myEditor.getSelectionModel().addSelectionListener(selectionListener);
719     myEditor.addEditorMouseListener(mouseListener);
720     Disposer.register(this, new Disposable() {
721       @Override
722       public void dispose() {
723         myEditor.getCaretModel().removeCaretListener(caretListener);
724         myEditor.getSelectionModel().removeSelectionListener(selectionListener);
725         myEditor.removeEditorMouseListener(mouseListener);
726       }
727     });
728
729     JComponent editorComponent = myEditor.getContentComponent();
730     if (editorComponent.isShowing()) {
731       Disposer.register(this, new UiNotifyConnector(editorComponent, new Activatable() {
732         @Override
733         public void showNotify() {
734         }
735   
736         @Override
737         public void hideNotify() {
738           hideLookup(false);
739         }
740       }));
741     }
742
743     myList.addListSelectionListener(new ListSelectionListener() {
744       private LookupElement oldItem = null;
745
746       @Override
747       public void valueChanged(@NotNull ListSelectionEvent e){
748         if (!myUpdating) {
749           final LookupElement item = getCurrentItem();
750           fireCurrentItemChanged(oldItem, item);
751           oldItem = item;
752         }
753       }
754
755     });
756
757     new ClickListener() {
758       @Override
759       public boolean onClick(@NotNull MouseEvent e, int clickCount) {
760         setFocusDegree(FocusDegree.FOCUSED);
761         markSelectionTouched();
762
763         if (clickCount == 2){
764           CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
765             @Override
766             public void run() {
767               finishLookup(NORMAL_SELECT_CHAR);
768             }
769           }, "", null);
770         }
771         return true;
772       }
773     }.installOn(myList);
774   }
775
776   @Override
777   @Nullable
778   public LookupElement getCurrentItem(){
779     LookupElement item = (LookupElement)myList.getSelectedValue();
780     return item instanceof EmptyLookupItem ? null : item;
781   }
782
783   @Override
784   public void setCurrentItem(LookupElement item){
785     markSelectionTouched();
786     myList.setSelectedValue(item, false);
787   }
788
789   @Override
790   public void addLookupListener(LookupListener listener){
791     myListeners.add(listener);
792   }
793
794   @Override
795   public void removeLookupListener(LookupListener listener){
796     myListeners.remove(listener);
797   }
798
799   @Override
800   public Rectangle getCurrentItemBounds(){
801     int index = myList.getSelectedIndex();
802     if (index < 0) {
803       LOG.error("No selected element, size=" + getListModel().getSize() + "; items" + getItems());
804     }
805     Rectangle itmBounds = myList.getCellBounds(index, index);
806     if (itmBounds == null){
807       LOG.error("No bounds for " + index + "; size=" + getListModel().getSize());
808       return null;
809     }
810     Point layeredPanePoint=SwingUtilities.convertPoint(myList,itmBounds.x,itmBounds.y,getComponent());
811     itmBounds.x = layeredPanePoint.x;
812     itmBounds.y = layeredPanePoint.y;
813     return itmBounds;
814   }
815
816   public void fireItemSelected(@Nullable final LookupElement item, char completionChar){
817     PsiDocumentManager.getInstance(myProject).commitAllDocuments();
818
819     if (!myListeners.isEmpty()){
820       LookupEvent event = new LookupEvent(this, item, completionChar);
821       for (LookupListener listener : myListeners) {
822         try {
823           listener.itemSelected(event);
824         }
825         catch (Throwable e) {
826           LOG.error(e);
827         }
828       }
829     }
830   }
831
832   private void fireLookupCanceled(final boolean explicitly) {
833     if (!myListeners.isEmpty()){
834       LookupEvent event = new LookupEvent(this, explicitly);
835       for (LookupListener listener : myListeners) {
836         try {
837           listener.lookupCanceled(event);
838         }
839         catch (Throwable e) {
840           LOG.error(e);
841         }
842       }
843     }
844   }
845
846   private void fireCurrentItemChanged(@Nullable LookupElement oldItem, @Nullable LookupElement currentItem) {
847     if (oldItem != currentItem && !myListeners.isEmpty()) {
848       LookupEvent event = new LookupEvent(this, currentItem, (char)0);
849       for (LookupListener listener : myListeners) {
850         listener.currentItemChanged(event);
851       }
852     }
853   }
854
855   public boolean fillInCommonPrefix(boolean explicitlyInvoked) {
856     if (explicitlyInvoked) {
857       setFocusDegree(FocusDegree.FOCUSED);
858     }
859
860     if (explicitlyInvoked && myCalculating) return false;
861     if (!explicitlyInvoked && mySelectionTouched) return false;
862
863     ListModel listModel = getListModel();
864     if (listModel.getSize() <= 1) return false;
865
866     if (listModel.getSize() == 0) return false;
867
868     final LookupElement firstItem = (LookupElement)listModel.getElementAt(0);
869     if (listModel.getSize() == 1 && firstItem instanceof EmptyLookupItem) return false;
870
871     final PrefixMatcher firstItemMatcher = itemMatcher(firstItem);
872     final String oldPrefix = firstItemMatcher.getPrefix();
873     final String presentPrefix = oldPrefix + getAdditionalPrefix();
874     String commonPrefix = getCaseCorrectedLookupString(firstItem);
875
876     for (int i = 1; i < listModel.getSize(); i++) {
877       LookupElement item = (LookupElement)listModel.getElementAt(i);
878       if (item instanceof EmptyLookupItem) return false;
879       if (!oldPrefix.equals(itemMatcher(item).getPrefix())) return false;
880
881       final String lookupString = getCaseCorrectedLookupString(item);
882       final int length = Math.min(commonPrefix.length(), lookupString.length());
883       if (length < commonPrefix.length()) {
884         commonPrefix = commonPrefix.substring(0, length);
885       }
886
887       for (int j = 0; j < length; j++) {
888         if (commonPrefix.charAt(j) != lookupString.charAt(j)) {
889           commonPrefix = lookupString.substring(0, j);
890           break;
891         }
892       }
893
894       if (commonPrefix.length() == 0 || commonPrefix.length() < presentPrefix.length()) {
895         return false;
896       }
897     }
898
899     if (commonPrefix.equals(presentPrefix)) {
900       return false;
901     }
902
903     for (int i = 0; i < listModel.getSize(); i++) {
904       LookupElement item = (LookupElement)listModel.getElementAt(i);
905       if (!itemMatcher(item).cloneWithPrefix(commonPrefix).prefixMatches(item)) {
906         return false;
907       }
908     }
909
910     myOffsets.setInitialPrefix(presentPrefix, explicitlyInvoked);
911
912     replacePrefix(presentPrefix, commonPrefix);
913     return true;
914   }
915
916   public void replacePrefix(final String presentPrefix, final String newPrefix) {
917     if (!performGuardedChange(new Runnable() {
918       @Override
919       public void run() {
920         EditorModificationUtil.deleteSelectedText(myEditor);
921         int offset = myEditor.getCaretModel().getOffset();
922         final int start = offset - presentPrefix.length();
923         myEditor.getDocument().replaceString(start, offset, newPrefix);
924
925         Map<LookupElement, PrefixMatcher> newMatchers = new HashMap<LookupElement, PrefixMatcher>();
926         for (LookupElement item : getItems()) {
927           if (item.isValid()) {
928             PrefixMatcher matcher = itemMatcher(item).cloneWithPrefix(newPrefix);
929             if (matcher.prefixMatches(item)) {
930               newMatchers.put(item, matcher);
931             }
932           }
933         }
934         myMatchers.clear();
935         myMatchers.putAll(newMatchers);
936
937         myOffsets.clearAdditionalPrefix();
938
939         myEditor.getCaretModel().moveToOffset(start + newPrefix.length());
940       }
941     })) {
942       return;
943     }
944     synchronized (myList) {
945       myPresentableArranger.prefixChanged(this);
946     }
947     refreshUi(true, true);
948   }
949
950   @Override
951   @Nullable
952   public PsiFile getPsiFile() {
953     return PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
954   }
955
956   @Override
957   public boolean isCompletion() {
958     return myArranger instanceof CompletionLookupArranger;
959   }
960
961   @Override
962   public PsiElement getPsiElement() {
963     PsiFile file = getPsiFile();
964     if (file == null) return null;
965
966     int offset = getLookupStart();
967     if (offset > 0) return file.findElementAt(offset - 1);
968
969     return file.findElementAt(0);
970   }
971
972   @Override
973   @NotNull 
974   public Editor getEditor() {
975     return myEditor;
976   }
977
978   @Override
979   public boolean isPositionedAboveCaret(){
980     return myUi != null && myUi.isPositionedAboveCaret();
981   }
982
983   @Override
984   public boolean isSelectionTouched() {
985     return mySelectionTouched;
986   }
987
988   @Override
989   public List<String> getAdvertisements() {
990     return myAdComponent.getAdvertisements();
991   }
992
993   @Override
994   public void hide(){
995     hideLookup(true);
996   }
997
998   public void hideLookup(boolean explicitly) {
999     ApplicationManager.getApplication().assertIsDispatchThread();
1000
1001     if (myHidden) return;
1002
1003     doHide(true, explicitly);
1004   }
1005
1006   private void doHide(final boolean fireCanceled, final boolean explicitly) {
1007     if (myDisposed) {
1008       LOG.error(disposeTrace);
1009     }
1010     else {
1011       myHidden = true;
1012
1013       try {
1014         super.hide();
1015
1016         Disposer.dispose(this);
1017
1018         assert myDisposed;
1019       }
1020       catch (Throwable e) {
1021         LOG.error(e);
1022       }
1023     }
1024
1025     if (fireCanceled) {
1026       fireLookupCanceled(explicitly);
1027     }
1028   }
1029
1030   public void restorePrefix() {
1031     myOffsets.restorePrefix();
1032   }
1033
1034   private static String staticDisposeTrace = null;
1035   private String disposeTrace = null;
1036
1037   public static String getLastLookupDisposeTrace() {
1038     return staticDisposeTrace;
1039   }
1040
1041   @Override
1042   public void dispose() {
1043     assert ApplicationManager.getApplication().isDispatchThread();
1044     assert myHidden;
1045     if (myDisposed) {
1046       LOG.error(disposeTrace);
1047       return;
1048     }
1049
1050     myOffsets.disposeMarkers();
1051     myDisposed = true;
1052     disposeTrace = DebugUtil.currentStackTrace() + "\n============";
1053     //noinspection AssignmentToStaticFieldFromInstanceMethod
1054     staticDisposeTrace = disposeTrace;
1055   }
1056
1057   public void refreshUi(boolean mayCheckReused, boolean onExplicitAction) {
1058     assert !myUpdating;
1059     LookupElement prevItem = getCurrentItem();
1060     myUpdating = true;
1061     try {
1062       final boolean reused = mayCheckReused && checkReused();
1063       boolean selectionVisible = isSelectionVisible();
1064       boolean itemsChanged = updateList(onExplicitAction, reused);
1065       if (isVisible()) {
1066         LOG.assertTrue(!ApplicationManager.getApplication().isUnitTestMode());
1067         myUi.refreshUi(selectionVisible, itemsChanged, reused, onExplicitAction);
1068       }
1069     }
1070     finally {
1071       myUpdating = false;
1072       fireCurrentItemChanged(prevItem, getCurrentItem());
1073     }
1074   }
1075
1076   public void markReused() {
1077     synchronized (myList) {
1078       myArranger = myArranger.createEmptyCopy();
1079     }
1080     requestResize();
1081   }
1082
1083   public void addAdvertisement(@NotNull final String text, final @Nullable Color bgColor) {
1084     if (containsDummyIdentifier(text)) {
1085       return;
1086     }
1087
1088     myAdComponent.addAdvertisement(text, bgColor);
1089     requestResize();
1090   }
1091
1092   public boolean isLookupDisposed() {
1093     return myDisposed;
1094   }
1095
1096   public void checkValid() {
1097     if (myDisposed) {
1098       throw new AssertionError("Disposed at: " + disposeTrace);
1099     }
1100   }
1101
1102   @Override
1103   public void showItemPopup(JBPopup hint) {
1104     final Rectangle bounds = getCurrentItemBounds();
1105     hint.show(new RelativePoint(getComponent(), new Point(bounds.x + bounds.width, bounds.y)));
1106   }
1107
1108   @Override
1109   public boolean showElementActions() {
1110     if (!isVisible()) return false;
1111
1112     final LookupElement element = getCurrentItem();
1113     if (element == null) {
1114       return false;
1115     }
1116
1117     final Collection<LookupElementAction> actions = getActionsFor(element);
1118     if (actions.isEmpty()) {
1119       return false;
1120     }
1121
1122     showItemPopup(JBPopupFactory.getInstance().createListPopup(new LookupActionsStep(actions, this, element)));
1123     return true;
1124   }
1125
1126   public Map<LookupElement,StringBuilder> getRelevanceStrings() {
1127     synchronized (myList) {
1128       return myPresentableArranger.getRelevanceStrings();
1129     }
1130   }
1131
1132   public enum FocusDegree { FOCUSED, SEMI_FOCUSED, UNFOCUSED }
1133
1134 }