2 * Copyright 2000-2015 JetBrains s.r.o.
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package com.intellij.codeInsight.lookup.impl;
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;
63 import javax.swing.event.ListSelectionEvent;
64 import javax.swing.event.ListSelectionListener;
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;
73 public class LookupImpl extends LightweightHint implements LookupEx, Disposable, WeighingContext {
74 private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupImpl");
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>()) {
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() {
87 IdeEventQueue.getInstance().getKeyEventDispatcher().dispatchKeyEvent(e);
93 super.processKeyEvent(e);
98 protected ExpandableItemsHandler<Integer> createExpandableItemsHandler() {
99 return new CompletionExtender(this);
102 final LookupCellRenderer myCellRenderer;
104 private final List<LookupListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
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;
126 private LookupUi myUi;
128 public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger) {
129 super(new JPanel(new BorderLayout()));
130 setForceShowAsPopup(true);
131 setCancelOnClickOutside(false);
133 AbstractPopup.suppressMacCornerFor(getComponent());
137 myArranger = arranger;
138 myPresentableArranger = arranger;
140 DaemonCodeAnalyzer.getInstance(myProject).disableUpdateByTimer(this);
142 myCellRenderer = new LookupCellRenderer(this);
143 myList.setCellRenderer(myCellRenderer);
145 myList.setFocusable(false);
146 myList.setFixedCellWidth(50);
148 myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
149 myList.setBackground(LookupCellRenderer.BACKGROUND_COLOR);
151 myList.getExpandableItemsHandler();
153 myAdComponent = new Advertiser();
155 myOffsets = new LookupOffsets(editor);
157 final CollectionListModel<LookupElement> model = getListModel();
159 updateListHeight(model);
164 private CollectionListModel<LookupElement> getListModel() {
165 //noinspection unchecked
166 return (CollectionListModel<LookupElement>)myList.getModel();
169 public void setArranger(LookupArranger arranger) {
170 myArranger = arranger;
173 public FocusDegree getFocusDegree() {
174 return myFocusDegree;
178 public boolean isFocused() {
179 return getFocusDegree() == FocusDegree.FOCUSED;
182 public void setFocusDegree(FocusDegree focusDegree) {
183 myFocusDegree = focusDegree;
186 public boolean isCalculating() {
187 return myCalculating;
190 public void setCalculating(final boolean calculating) {
191 myCalculating = calculating;
193 myUi.setCalculating(calculating);
197 public void markSelectionTouched() {
198 if (!ApplicationManager.getApplication().isUnitTestMode()) {
199 ApplicationManager.getApplication().assertIsDispatchThread();
201 mySelectionTouched = true;
206 public void setSelectionTouched(boolean selectionTouched) {
207 mySelectionTouched = selectionTouched;
210 public void resort(boolean addAgain) {
211 final List<LookupElement> items = getItems();
213 synchronized (myList) {
214 myPresentableArranger.prefixChanged(this);
215 getListModel().removeAll();
219 for (final LookupElement item : items) {
220 addItem(item, itemMatcher(item));
223 refreshUi(true, true);
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())) {
234 myMatchers.put(item, matcher);
235 updateLookupWidth(item, presentation);
236 synchronized (myList) {
237 myArranger.addElement(this, item, presentation);
242 private static boolean containsDummyIdentifier(@Nullable final String s) {
243 return s != null && s.contains(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED);
246 public void updateLookupWidth(LookupElement item) {
247 updateLookupWidth(item, renderItemApproximately(item));
250 private void updateLookupWidth(LookupElement item, LookupElementPresentation presentation) {
251 final Font customFont = myCellRenderer.getFontAbleToDisplay(presentation);
252 if (customFont != null) {
253 myCustomFonts.put(item, customFont);
255 int maxWidth = myCellRenderer.updateMaximumWidth(presentation, item);
256 myLookupTextWidth = Math.max(maxWidth, myLookupTextWidth);
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;
265 public void requestResize() {
266 ApplicationManager.getApplication().assertIsDispatchThread();
267 myResizePending = true;
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);
275 if (!consumer.getResult().isEmpty()) {
276 consumer.consume(new ShowHideIntentionIconLookupAction());
278 return consumer.getResult();
281 public JList getList() {
286 public List<LookupElement> getItems() {
287 synchronized (myList) {
288 return ContainerUtil.findAll(getListModel().toList(), new Condition<LookupElement>() {
290 public boolean value(LookupElement element) {
291 return !(element instanceof EmptyLookupItem);
297 public String getAdditionalPrefix() {
298 return myOffsets.getAdditionalPrefix();
301 void appendPrefix(char c) {
303 myOffsets.appendPrefix(c);
304 synchronized (myList) {
305 myPresentableArranger.prefixChanged(this);
308 refreshUi(false, true);
309 ensureSelectionVisible(true);
312 public void setStartCompletionWhenNothingMatches(boolean startCompletionWhenNothingMatches) {
313 myStartCompletionWhenNothingMatches = startCompletionWhenNothingMatches;
316 public boolean isStartCompletionWhenNothingMatches() {
317 return myStartCompletionWhenNothingMatches;
320 public void ensureSelectionVisible(boolean forceTopSelection) {
321 if (isSelectionVisible() && !forceTopSelection) {
325 if (!forceTopSelection) {
326 ScrollingUtil.ensureIndexIsVisible(myList, myList.getSelectedIndex(), 1);
330 // selected item should be at the top of the visible list
331 int top = myList.getSelectedIndex();
333 top--; // show one element above the selected one to give the hint that there are more available via scrolling
336 int firstVisibleIndex = myList.getFirstVisibleIndex();
337 if (firstVisibleIndex == top) {
341 ScrollingUtil.ensureRangeIsVisible(myList, top, top + myList.getLastVisibleIndex() - firstVisibleIndex);
344 boolean truncatePrefix(boolean preserveSelection) {
345 if (!myOffsets.truncatePrefix()) {
349 if (preserveSelection) {
350 markSelectionTouched();
353 boolean shouldUpdate;
354 synchronized (myList) {
355 shouldUpdate = myPresentableArranger == myArranger;
356 myPresentableArranger.prefixChanged(this);
360 refreshUi(false, true);
361 ensureSelectionVisible(true);
367 private boolean updateList(boolean onExplicitAction, boolean reused) {
368 if (!ApplicationManager.getApplication().isUnitTestMode()) {
369 ApplicationManager.getApplication().assertIsDispatchThread();
373 CollectionListModel<LookupElement> listModel = getListModel();
375 Pair<List<LookupElement>, Integer> pair;
376 synchronized (myList) {
377 pair = myPresentableArranger.arrangeItems(this, onExplicitAction || reused);
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);
387 myOffsets.checkMinPrefixLengthChanges(items, this);
388 List<LookupElement> oldModel = listModel.toList();
390 listModel.removeAll();
391 if (!items.isEmpty()) {
392 listModel.add(items);
395 addEmptyItem(listModel);
398 updateListHeight(listModel);
400 myList.setSelectedIndex(toSelect);
401 return !ContainerUtil.equalsIdentity(oldModel, items);
404 private boolean isSelectionVisible() {
405 return ScrollingUtil.isIndexFullyVisible(myList, myList.getSelectedIndex());
408 private boolean checkReused() {
409 synchronized (myList) {
410 if (myPresentableArranger != myArranger) {
411 myPresentableArranger = myArranger;
412 myOffsets.clearAdditionalPrefix();
413 myPresentableArranger.prefixChanged(this);
420 private void updateListHeight(ListModel model) {
421 myList.setFixedCellHeight(myCellRenderer.getListCellRendererComponent(myList, model.getElementAt(0), 0, false, false).getPreferredSize().height);
423 myList.setVisibleRowCount(Math.min(model.getSize(), UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT));
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(""));
431 updateLookupWidth(item);
435 private static LookupElementPresentation renderItemApproximately(LookupElement item) {
436 final LookupElementPresentation p = new LookupElementPresentation();
437 item.renderElement(p);
443 public String itemPattern(@NotNull LookupElement element) {
444 String prefix = itemMatcher(element).getPrefix();
445 String additionalPrefix = getAdditionalPrefix();
446 return additionalPrefix.isEmpty() ? prefix : prefix + additionalPrefix;
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());
459 public PrefixMatcher itemMatcherNullable(LookupElement item) {
460 return myMatchers.get(item);
463 public void finishLookup(final char completionChar) {
464 finishLookup(completionChar, (LookupElement)myList.getSelectedValue());
467 public void finishLookup(char completionChar, @Nullable final LookupElement item) {
468 //noinspection deprecation,unchecked
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)) {
475 fireItemSelected(null, completionChar);
479 if (myDisposed) { // DeferredUserLookupValue could close us in any way
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
491 fireItemSelected(null, completionChar);
495 final String prefix = itemPattern(item);
496 boolean plainMatch = ContainerUtil.or(item.getAllLookupStrings(), new Condition<String>() {
498 public boolean value(String s) {
499 return StringUtil.containsIgnoreCase(s, prefix);
503 FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CAMEL_HUMPS);
507 ApplicationManager.getApplication().runWriteAction(new Runnable() {
509 myEditor.getDocument().startGuardedBlockChecking();
511 insertLookupString(item, getPrefixLength(item));
514 myEditor.getDocument().stopGuardedBlockChecking();
519 if (myDisposed) { // any document listeners could close us
525 fireItemSelected(item, completionChar);
528 public int getPrefixLength(LookupElement item) {
529 return myOffsets.getPrefixLength(item, this);
532 private void insertLookupString(LookupElement item, final int prefix) {
533 final String lookupString = getCaseCorrectedLookupString(item);
535 final Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor);
536 hostEditor.getCaretModel().runForEachCaret(new CaretAction() {
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));
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);
548 hostEditor.getDocument().replaceString(lookupStart, caretOffset, lookupString);
550 int offset = lookupStart + lookupString.length();
551 hostEditor.getCaretModel().moveToOffset(offset);
552 hostEditor.getSelectionModel().removeSelection();
556 myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
559 private String getCaseCorrectedLookupString(LookupElement item) {
560 String lookupString = item.getLookupString();
561 if (item.isCaseSensitive()) {
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));
581 if (sameCase) return lookupString;
582 if (isAllLower) return lookupString.toLowerCase();
583 if (isAllUpper) return StringUtil.toUpperCase(lookupString);
588 public int getLookupStart() {
589 return myOffsets.getLookupStart(disposeTrace);
592 public int getLookupOriginalStart() {
593 return myOffsets.getLookupOriginalStart();
596 public boolean performGuardedChange(Runnable change) {
598 assert !myChangeGuard : "already in change";
600 myEditor.getDocument().startGuardedBlockChecking();
601 myChangeGuard = true;
604 result = myOffsets.performGuardedChange(change);
607 myEditor.getDocument().stopGuardedBlockChecking();
608 myChangeGuard = false;
610 if (!result || myDisposed) {
615 HintManagerImpl.updateLocation(this, myEditor, myUi.calculatePosition().getLocation());
622 public boolean vetoesHiding() {
623 return myChangeGuard;
626 public boolean isAvailableToUser() {
627 if (ApplicationManager.getApplication().isUnitTestMode()) {
633 public boolean isShown() {
634 if (!ApplicationManager.getApplication().isUnitTestMode()) {
635 ApplicationManager.getApplication().assertIsDispatchThread();
640 public boolean showLookup() {
641 ApplicationManager.getApplication().assertIsDispatchThread();
643 LOG.assertTrue(!myShown);
645 myStampShown = System.currentTimeMillis();
647 if (ApplicationManager.getApplication().isUnitTestMode()) return true;
649 if (!myEditor.getContentComponent().isShowing()) {
654 myAdComponent.showRandomText();
656 myUi = new LookupUi(this, myAdComponent, myList, myProject);
657 myUi.setCalculating(myCalculating);
658 Point p = myUi.calculatePosition().getLocation();
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));
663 catch (Exception e) {
667 if (!isVisible() || !myList.isShowing()) {
675 public Advertiser getAdvertiser() {
676 return myAdComponent;
679 public boolean mayBeNoticed() {
680 return myStampShown > 0 && System.currentTimeMillis() - myStampShown > 300;
683 private void addListeners() {
684 myEditor.getDocument().addDocumentListener(new DocumentAdapter() {
686 public void documentChanged(DocumentEvent e) {
687 if (!myChangeGuard && !myFinishing) {
693 final CaretListener caretListener = new CaretAdapter() {
695 public void caretPositionChanged(CaretEvent e) {
696 if (!myChangeGuard && !myFinishing) {
701 final SelectionListener selectionListener = new SelectionListener() {
703 public void selectionChanged(final SelectionEvent e) {
704 if (!myChangeGuard && !myFinishing) {
709 final EditorMouseListener mouseListener = new EditorMouseAdapter() {
711 public void mouseClicked(EditorMouseEvent e){
717 myEditor.getCaretModel().addCaretListener(caretListener);
718 myEditor.getSelectionModel().addSelectionListener(selectionListener);
719 myEditor.addEditorMouseListener(mouseListener);
720 Disposer.register(this, new Disposable() {
722 public void dispose() {
723 myEditor.getCaretModel().removeCaretListener(caretListener);
724 myEditor.getSelectionModel().removeSelectionListener(selectionListener);
725 myEditor.removeEditorMouseListener(mouseListener);
729 JComponent editorComponent = myEditor.getContentComponent();
730 if (editorComponent.isShowing()) {
731 Disposer.register(this, new UiNotifyConnector(editorComponent, new Activatable() {
733 public void showNotify() {
737 public void hideNotify() {
743 myList.addListSelectionListener(new ListSelectionListener() {
744 private LookupElement oldItem = null;
747 public void valueChanged(@NotNull ListSelectionEvent e){
749 final LookupElement item = getCurrentItem();
750 fireCurrentItemChanged(oldItem, item);
757 new ClickListener() {
759 public boolean onClick(@NotNull MouseEvent e, int clickCount) {
760 setFocusDegree(FocusDegree.FOCUSED);
761 markSelectionTouched();
763 if (clickCount == 2){
764 CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
767 finishLookup(NORMAL_SELECT_CHAR);
778 public LookupElement getCurrentItem(){
779 LookupElement item = (LookupElement)myList.getSelectedValue();
780 return item instanceof EmptyLookupItem ? null : item;
784 public void setCurrentItem(LookupElement item){
785 markSelectionTouched();
786 myList.setSelectedValue(item, false);
790 public void addLookupListener(LookupListener listener){
791 myListeners.add(listener);
795 public void removeLookupListener(LookupListener listener){
796 myListeners.remove(listener);
800 public Rectangle getCurrentItemBounds(){
801 int index = myList.getSelectedIndex();
803 LOG.error("No selected element, size=" + getListModel().getSize() + "; items" + getItems());
805 Rectangle itmBounds = myList.getCellBounds(index, index);
806 if (itmBounds == null){
807 LOG.error("No bounds for " + index + "; size=" + getListModel().getSize());
810 Point layeredPanePoint=SwingUtilities.convertPoint(myList,itmBounds.x,itmBounds.y,getComponent());
811 itmBounds.x = layeredPanePoint.x;
812 itmBounds.y = layeredPanePoint.y;
816 public void fireItemSelected(@Nullable final LookupElement item, char completionChar){
817 PsiDocumentManager.getInstance(myProject).commitAllDocuments();
819 if (!myListeners.isEmpty()){
820 LookupEvent event = new LookupEvent(this, item, completionChar);
821 for (LookupListener listener : myListeners) {
823 listener.itemSelected(event);
825 catch (Throwable e) {
832 private void fireLookupCanceled(final boolean explicitly) {
833 if (!myListeners.isEmpty()){
834 LookupEvent event = new LookupEvent(this, explicitly);
835 for (LookupListener listener : myListeners) {
837 listener.lookupCanceled(event);
839 catch (Throwable e) {
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);
855 public boolean fillInCommonPrefix(boolean explicitlyInvoked) {
856 if (explicitlyInvoked) {
857 setFocusDegree(FocusDegree.FOCUSED);
860 if (explicitlyInvoked && myCalculating) return false;
861 if (!explicitlyInvoked && mySelectionTouched) return false;
863 ListModel listModel = getListModel();
864 if (listModel.getSize() <= 1) return false;
866 if (listModel.getSize() == 0) return false;
868 final LookupElement firstItem = (LookupElement)listModel.getElementAt(0);
869 if (listModel.getSize() == 1 && firstItem instanceof EmptyLookupItem) return false;
871 final PrefixMatcher firstItemMatcher = itemMatcher(firstItem);
872 final String oldPrefix = firstItemMatcher.getPrefix();
873 final String presentPrefix = oldPrefix + getAdditionalPrefix();
874 String commonPrefix = getCaseCorrectedLookupString(firstItem);
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;
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);
887 for (int j = 0; j < length; j++) {
888 if (commonPrefix.charAt(j) != lookupString.charAt(j)) {
889 commonPrefix = lookupString.substring(0, j);
894 if (commonPrefix.length() == 0 || commonPrefix.length() < presentPrefix.length()) {
899 if (commonPrefix.equals(presentPrefix)) {
903 for (int i = 0; i < listModel.getSize(); i++) {
904 LookupElement item = (LookupElement)listModel.getElementAt(i);
905 if (!itemMatcher(item).cloneWithPrefix(commonPrefix).prefixMatches(item)) {
910 myOffsets.setInitialPrefix(presentPrefix, explicitlyInvoked);
912 replacePrefix(presentPrefix, commonPrefix);
916 public void replacePrefix(final String presentPrefix, final String newPrefix) {
917 if (!performGuardedChange(new Runnable() {
920 EditorModificationUtil.deleteSelectedText(myEditor);
921 int offset = myEditor.getCaretModel().getOffset();
922 final int start = offset - presentPrefix.length();
923 myEditor.getDocument().replaceString(start, offset, newPrefix);
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);
935 myMatchers.putAll(newMatchers);
937 myOffsets.clearAdditionalPrefix();
939 myEditor.getCaretModel().moveToOffset(start + newPrefix.length());
944 synchronized (myList) {
945 myPresentableArranger.prefixChanged(this);
947 refreshUi(true, true);
952 public PsiFile getPsiFile() {
953 return PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
957 public boolean isCompletion() {
958 return myArranger instanceof CompletionLookupArranger;
962 public PsiElement getPsiElement() {
963 PsiFile file = getPsiFile();
964 if (file == null) return null;
966 int offset = getLookupStart();
967 if (offset > 0) return file.findElementAt(offset - 1);
969 return file.findElementAt(0);
974 public Editor getEditor() {
979 public boolean isPositionedAboveCaret(){
980 return myUi != null && myUi.isPositionedAboveCaret();
984 public boolean isSelectionTouched() {
985 return mySelectionTouched;
989 public List<String> getAdvertisements() {
990 return myAdComponent.getAdvertisements();
998 public void hideLookup(boolean explicitly) {
999 ApplicationManager.getApplication().assertIsDispatchThread();
1001 if (myHidden) return;
1003 doHide(true, explicitly);
1006 private void doHide(final boolean fireCanceled, final boolean explicitly) {
1008 LOG.error(disposeTrace);
1016 Disposer.dispose(this);
1020 catch (Throwable e) {
1026 fireLookupCanceled(explicitly);
1030 public void restorePrefix() {
1031 myOffsets.restorePrefix();
1034 private static String staticDisposeTrace = null;
1035 private String disposeTrace = null;
1037 public static String getLastLookupDisposeTrace() {
1038 return staticDisposeTrace;
1042 public void dispose() {
1043 assert ApplicationManager.getApplication().isDispatchThread();
1046 LOG.error(disposeTrace);
1050 myOffsets.disposeMarkers();
1052 disposeTrace = DebugUtil.currentStackTrace() + "\n============";
1053 //noinspection AssignmentToStaticFieldFromInstanceMethod
1054 staticDisposeTrace = disposeTrace;
1057 public void refreshUi(boolean mayCheckReused, boolean onExplicitAction) {
1059 LookupElement prevItem = getCurrentItem();
1062 final boolean reused = mayCheckReused && checkReused();
1063 boolean selectionVisible = isSelectionVisible();
1064 boolean itemsChanged = updateList(onExplicitAction, reused);
1066 LOG.assertTrue(!ApplicationManager.getApplication().isUnitTestMode());
1067 myUi.refreshUi(selectionVisible, itemsChanged, reused, onExplicitAction);
1072 fireCurrentItemChanged(prevItem, getCurrentItem());
1076 public void markReused() {
1077 synchronized (myList) {
1078 myArranger = myArranger.createEmptyCopy();
1083 public void addAdvertisement(@NotNull final String text, final @Nullable Color bgColor) {
1084 if (containsDummyIdentifier(text)) {
1088 myAdComponent.addAdvertisement(text, bgColor);
1092 public boolean isLookupDisposed() {
1096 public void checkValid() {
1098 throw new AssertionError("Disposed at: " + disposeTrace);
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)));
1109 public boolean showElementActions() {
1110 if (!isVisible()) return false;
1112 final LookupElement element = getCurrentItem();
1113 if (element == null) {
1117 final Collection<LookupElementAction> actions = getActionsFor(element);
1118 if (actions.isEmpty()) {
1122 showItemPopup(JBPopupFactory.getInstance().createListPopup(new LookupActionsStep(actions, this, element)));
1126 public Map<LookupElement,StringBuilder> getRelevanceStrings() {
1127 synchronized (myList) {
1128 return myPresentableArranger.getRelevanceStrings();
1132 public enum FocusDegree { FOCUSED, SEMI_FOCUSED, UNFOCUSED }