blob: 869886d26523355eea4bc90a9620b6d9ba9a1ad8 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.codeInsight.lookup.impl;
import com.intellij.codeInsight.FileModificationService;
import com.intellij.codeInsight.completion.*;
import com.intellij.codeInsight.completion.impl.CamelHumpMatcher;
import com.intellij.codeInsight.daemon.DaemonCodeAnalyzer;
import com.intellij.codeInsight.hint.HintManager;
import com.intellij.codeInsight.hint.HintManagerImpl;
import com.intellij.codeInsight.lookup.*;
import com.intellij.featureStatistics.FeatureUsageTracker;
import com.intellij.ide.IdeEventQueue;
import com.intellij.ide.ui.UISettings;
import com.intellij.lang.LangBundle;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.event.DocumentAdapter;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.popup.JBPopup;
import com.intellij.openapi.ui.popup.JBPopupFactory;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.wm.IdeFocusManager;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.DebugUtil;
import com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil;
import com.intellij.ui.*;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.ui.components.JBList;
import com.intellij.ui.popup.AbstractPopup;
import com.intellij.util.CollectConsumer;
import com.intellij.util.containers.ConcurrentHashMap;
import com.intellij.util.containers.ConcurrentWeakHashMap;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.ui.update.Activatable;
import com.intellij.util.ui.update.UiNotifyConnector;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.TestOnly;
import javax.swing.*;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class LookupImpl extends LightweightHint implements LookupEx, Disposable, WeighingContext {
private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.lookup.impl.LookupImpl");
private final LookupOffsets myOffsets;
private final Project myProject;
private final Editor myEditor;
private final JBList myList = new JBList(new CollectionListModel<LookupElement>()) {
@Override
protected void processKeyEvent(@NotNull final KeyEvent e) {
final char keyChar = e.getKeyChar();
if (keyChar == KeyEvent.VK_ENTER || keyChar == KeyEvent.VK_TAB) {
IdeFocusManager.getInstance(myProject).requestFocus(myEditor.getContentComponent(), true).doWhenDone(new Runnable() {
@Override
public void run() {
IdeEventQueue.getInstance().getKeyEventDispatcher().dispatchKeyEvent(e);
}
});
return;
}
super.processKeyEvent(e);
}
ExpandableItemsHandler<Integer> myExtender = new CompletionExtender(this);
@NotNull
@Override
public ExpandableItemsHandler<Integer> getExpandableItemsHandler() {
return myExtender;
}
};
final LookupCellRenderer myCellRenderer;
private final List<LookupListener> myListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private long myStampShown = 0;
private boolean myShown = false;
private boolean myDisposed = false;
private boolean myHidden = false;
private boolean mySelectionTouched;
private FocusDegree myFocusDegree = FocusDegree.FOCUSED;
private volatile boolean myCalculating;
private final Advertiser myAdComponent;
volatile int myLookupTextWidth = 50;
private boolean myChangeGuard;
private volatile LookupArranger myArranger;
private LookupArranger myPresentableArranger;
private final Map<LookupElement, PrefixMatcher> myMatchers = new ConcurrentHashMap<LookupElement, PrefixMatcher>(
ContainerUtil.<LookupElement>identityStrategy());
private final Map<LookupElement, Font> myCustomFonts = new ConcurrentWeakHashMap<LookupElement, Font>(
ContainerUtil.<LookupElement>identityStrategy());
private boolean myStartCompletionWhenNothingMatches;
boolean myResizePending;
private boolean myFinishing;
boolean myUpdating;
private LookupUi myUi;
public LookupImpl(Project project, Editor editor, @NotNull LookupArranger arranger) {
super(new JPanel(new BorderLayout()));
setForceShowAsPopup(true);
setCancelOnClickOutside(false);
setResizable(true);
AbstractPopup.suppressMacCornerFor(getComponent());
myProject = project;
myEditor = editor;
myArranger = arranger;
myPresentableArranger = arranger;
myCellRenderer = new LookupCellRenderer(this);
myList.setCellRenderer(myCellRenderer);
myList.setFocusable(false);
myList.setFixedCellWidth(50);
myList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
myList.setBackground(LookupCellRenderer.BACKGROUND_COLOR);
myList.getExpandableItemsHandler();
myAdComponent = new Advertiser();
myOffsets = new LookupOffsets(editor);
final CollectionListModel<LookupElement> model = getListModel();
addEmptyItem(model);
updateListHeight(model);
addListeners();
}
private CollectionListModel<LookupElement> getListModel() {
//noinspection unchecked
return (CollectionListModel<LookupElement>)myList.getModel();
}
public void setArranger(LookupArranger arranger) {
myArranger = arranger;
}
public FocusDegree getFocusDegree() {
return myFocusDegree;
}
@Override
public boolean isFocused() {
return getFocusDegree() == FocusDegree.FOCUSED;
}
public void setFocusDegree(FocusDegree focusDegree) {
myFocusDegree = focusDegree;
}
public boolean isCalculating() {
return myCalculating;
}
public void setCalculating(final boolean calculating) {
myCalculating = calculating;
if (myUi != null) {
myUi.setCalculating(calculating);
}
}
public void markSelectionTouched() {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
ApplicationManager.getApplication().assertIsDispatchThread();
}
mySelectionTouched = true;
myList.repaint();
}
@TestOnly
public void setSelectionTouched(boolean selectionTouched) {
mySelectionTouched = selectionTouched;
}
public void resort(boolean addAgain) {
final List<LookupElement> items = getItems();
synchronized (myList) {
myPresentableArranger.prefixChanged(this);
getListModel().removeAll();
}
if (addAgain) {
for (final LookupElement item : items) {
addItem(item, itemMatcher(item));
}
}
refreshUi(true, true);
}
public boolean addItem(LookupElement item, PrefixMatcher matcher) {
LookupElementPresentation presentation = renderItemApproximately(item);
if (containsDummyIdentifier(presentation.getItemText()) ||
containsDummyIdentifier(presentation.getTailText()) ||
containsDummyIdentifier(presentation.getTypeText())) {
return false;
}
myMatchers.put(item, matcher);
updateLookupWidth(item, presentation);
synchronized (myList) {
myArranger.addElement(this, item, presentation);
}
return true;
}
private static boolean containsDummyIdentifier(@Nullable final String s) {
return s != null && s.contains(CompletionUtil.DUMMY_IDENTIFIER_TRIMMED);
}
public void updateLookupWidth(LookupElement item) {
updateLookupWidth(item, renderItemApproximately(item));
}
private void updateLookupWidth(LookupElement item, LookupElementPresentation presentation) {
final Font customFont = myCellRenderer.getFontAbleToDisplay(presentation);
if (customFont != null) {
myCustomFonts.put(item, customFont);
}
int maxWidth = myCellRenderer.updateMaximumWidth(presentation, item);
myLookupTextWidth = Math.max(maxWidth, myLookupTextWidth);
}
@Nullable
public Font getCustomFont(LookupElement item, boolean bold) {
Font font = myCustomFonts.get(item);
return font == null ? null : bold ? font.deriveFont(Font.BOLD) : font;
}
public void requestResize() {
ApplicationManager.getApplication().assertIsDispatchThread();
myResizePending = true;
}
public Collection<LookupElementAction> getActionsFor(LookupElement element) {
final CollectConsumer<LookupElementAction> consumer = new CollectConsumer<LookupElementAction>();
for (LookupActionProvider provider : LookupActionProvider.EP_NAME.getExtensions()) {
provider.fillActions(element, this, consumer);
}
if (!consumer.getResult().isEmpty()) {
consumer.consume(new ShowHideIntentionIconLookupAction());
}
return consumer.getResult();
}
public JList getList() {
return myList;
}
@Override
public List<LookupElement> getItems() {
synchronized (myList) {
return ContainerUtil.findAll(getListModel().toList(), new Condition<LookupElement>() {
@Override
public boolean value(LookupElement element) {
return !(element instanceof EmptyLookupItem);
}
});
}
}
public String getAdditionalPrefix() {
return myOffsets.getAdditionalPrefix();
}
void appendPrefix(char c) {
checkValid();
myOffsets.appendPrefix(c);
synchronized (myList) {
myPresentableArranger.prefixChanged(this);
}
requestResize();
refreshUi(false, true);
ensureSelectionVisible(true);
}
public void setStartCompletionWhenNothingMatches(boolean startCompletionWhenNothingMatches) {
myStartCompletionWhenNothingMatches = startCompletionWhenNothingMatches;
}
public boolean isStartCompletionWhenNothingMatches() {
return myStartCompletionWhenNothingMatches;
}
public void ensureSelectionVisible(boolean forceTopSelection) {
if (isSelectionVisible() && !forceTopSelection) {
return;
}
if (!forceTopSelection) {
ListScrollingUtil.ensureIndexIsVisible(myList, myList.getSelectedIndex(), 1);
return;
}
// selected item should be at the top of the visible list
int top = myList.getSelectedIndex();
if (top > 0) {
top--; // show one element above the selected one to give the hint that there are more available via scrolling
}
int firstVisibleIndex = myList.getFirstVisibleIndex();
if (firstVisibleIndex == top) {
return;
}
ListScrollingUtil.ensureRangeIsVisible(myList, top, top + myList.getLastVisibleIndex() - firstVisibleIndex);
}
boolean truncatePrefix(boolean preserveSelection) {
if (!myOffsets.truncatePrefix()) {
return false;
}
if (preserveSelection) {
markSelectionTouched();
}
boolean shouldUpdate;
synchronized (myList) {
shouldUpdate = myPresentableArranger == myArranger;
myPresentableArranger.prefixChanged(this);
}
requestResize();
if (shouldUpdate) {
refreshUi(false, true);
ensureSelectionVisible(true);
}
return true;
}
private boolean updateList(boolean onExplicitAction, boolean reused) {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
ApplicationManager.getApplication().assertIsDispatchThread();
}
checkValid();
CollectionListModel<LookupElement> listModel = getListModel();
Pair<List<LookupElement>, Integer> pair;
synchronized (myList) {
pair = myPresentableArranger.arrangeItems(this, onExplicitAction || reused);
}
List<LookupElement> items = pair.first;
Integer toSelect = pair.second;
if (toSelect == null || toSelect < 0 || items.size() > 0 && toSelect >= items.size()) {
LOG.error("Arranger " + myPresentableArranger + " returned invalid selection index=" + toSelect + "; items=" + items);
toSelect = 0;
}
myOffsets.checkMinPrefixLengthChanges(items, this);
List<LookupElement> oldModel = listModel.toList();
listModel.removeAll();
if (!items.isEmpty()) {
listModel.add(items);
}
else {
addEmptyItem(listModel);
}
updateListHeight(listModel);
myList.setSelectedIndex(toSelect);
return !ContainerUtil.equalsIdentity(oldModel, items);
}
private boolean isSelectionVisible() {
return ListScrollingUtil.isIndexFullyVisible(myList, myList.getSelectedIndex());
}
private boolean checkReused() {
synchronized (myList) {
if (myPresentableArranger != myArranger) {
myPresentableArranger = myArranger;
myOffsets.clearAdditionalPrefix();
myPresentableArranger.prefixChanged(this);
return true;
}
return false;
}
}
private void updateListHeight(ListModel model) {
myList.setFixedCellHeight(myCellRenderer.getListCellRendererComponent(myList, model.getElementAt(0), 0, false, false).getPreferredSize().height);
myList.setVisibleRowCount(Math.min(model.getSize(), UISettings.getInstance().MAX_LOOKUP_LIST_HEIGHT));
}
private void addEmptyItem(CollectionListModel<LookupElement> model) {
LookupItem<String> item = new EmptyLookupItem(myCalculating ? " " : LangBundle.message("completion.no.suggestions"), false);
myMatchers.put(item, new CamelHumpMatcher(""));
model.add(item);
updateLookupWidth(item);
requestResize();
}
private static LookupElementPresentation renderItemApproximately(LookupElement item) {
final LookupElementPresentation p = new LookupElementPresentation();
item.renderElement(p);
return p;
}
@NotNull
@Override
public String itemPattern(@NotNull LookupElement element) {
String prefix = itemMatcher(element).getPrefix();
String additionalPrefix = getAdditionalPrefix();
return additionalPrefix.isEmpty() ? prefix : prefix + additionalPrefix;
}
@Override
@NotNull
public PrefixMatcher itemMatcher(@NotNull LookupElement item) {
PrefixMatcher matcher = itemMatcherNullable(item);
if (matcher == null) {
throw new AssertionError("Item not in lookup: item=" + item + "; lookup items=" + getItems());
}
return matcher;
}
public PrefixMatcher itemMatcherNullable(LookupElement item) {
return myMatchers.get(item);
}
public void finishLookup(final char completionChar) {
finishLookup(completionChar, (LookupElement)myList.getSelectedValue());
}
public void finishLookup(char completionChar, @Nullable final LookupElement item) {
//noinspection deprecation,unchecked
if (item == null ||
item instanceof EmptyLookupItem ||
item.getObject() instanceof DeferredUserLookupValue &&
item.as(LookupItem.CLASS_CONDITION_KEY) != null &&
!((DeferredUserLookupValue)item.getObject()).handleUserSelection(item.as(LookupItem.CLASS_CONDITION_KEY), myProject)) {
doHide(false, true);
fireItemSelected(null, completionChar);
return;
}
if (myDisposed) { // DeferredUserLookupValue could close us in any way
return;
}
final PsiFile file = getPsiFile();
boolean writableOk = file == null || FileModificationService.getInstance().prepareFileForWrite(file);
if (myDisposed) { // ensureFilesWritable could close us by showing a dialog
return;
}
if (!writableOk) {
doHide(false, true);
fireItemSelected(null, completionChar);
return;
}
final String prefix = itemPattern(item);
boolean plainMatch = ContainerUtil.or(item.getAllLookupStrings(), new Condition<String>() {
@Override
public boolean value(String s) {
return StringUtil.containsIgnoreCase(s, prefix);
}
});
if (!plainMatch) {
FeatureUsageTracker.getInstance().triggerFeatureUsed(CodeCompletionFeatures.EDITING_COMPLETION_CAMEL_HUMPS);
}
myFinishing = true;
ApplicationManager.getApplication().runWriteAction(new Runnable() {
public void run() {
myEditor.getDocument().startGuardedBlockChecking();
try {
insertLookupString(item, getPrefixLength(item));
}
finally {
myEditor.getDocument().stopGuardedBlockChecking();
}
}
});
if (myDisposed) { // any document listeners could close us
return;
}
doHide(false, true);
fireItemSelected(item, completionChar);
}
public int getPrefixLength(LookupElement item) {
return myOffsets.getPrefixLength(item, this);
}
private void insertLookupString(LookupElement item, final int prefix) {
final Document document = myEditor.getDocument();
final String lookupString = getCaseCorrectedLookupString(item);
if (myEditor.getSelectionModel().hasBlockSelection()) {
LogicalPosition blockStart = myEditor.getSelectionModel().getBlockStart();
LogicalPosition blockEnd = myEditor.getSelectionModel().getBlockEnd();
assert blockStart != null && blockEnd != null;
int minLine = Math.min(blockStart.line, blockEnd.line);
int maxLine = Math.max(blockStart.line, blockEnd.line);
int minColumn = Math.min(blockStart.column, blockEnd.column);
int maxColumn = Math.max(blockStart.column, blockEnd.column);
int caretLine = document.getLineNumber(myEditor.getCaretModel().getOffset());
for (int line = minLine; line <= maxLine; line++) {
int bs = myEditor.logicalPositionToOffset(new LogicalPosition(line, minColumn));
int start = bs - prefix;
int end = myEditor.logicalPositionToOffset(new LogicalPosition(line, maxColumn));
if (start > end) {
LOG.error("bs=" + bs + "; start=" + start + "; end=" + end +
"; blockStart=" + blockStart + "; blockEnd=" + blockEnd + "; line=" + line + "; len=" +
(document.getLineEndOffset(line) - document.getLineStartOffset(line)));
}
document.replaceString(start, end, lookupString);
}
LogicalPosition start = new LogicalPosition(minLine, minColumn - prefix);
LogicalPosition end = new LogicalPosition(maxLine, start.column + lookupString.length());
myEditor.getSelectionModel().setBlockSelection(start, end);
myEditor.getCaretModel().moveToLogicalPosition(new LogicalPosition(caretLine, end.column));
} else {
final Editor hostEditor = InjectedLanguageUtil.getTopLevelEditor(myEditor);
hostEditor.getCaretModel().runForEachCaret(new CaretAction() {
@Override
public void perform(Caret caret) {
EditorModificationUtil.deleteSelectedText(hostEditor);
final int caretOffset = hostEditor.getCaretModel().getOffset();
int lookupStart = Math.max(caretOffset - prefix, 0);
int len = hostEditor.getDocument().getTextLength();
LOG.assertTrue(lookupStart >= 0 && lookupStart <= len,
"ls: " + lookupStart + " caret: " + caretOffset + " prefix:" + prefix + " doc: " + len);
LOG.assertTrue(caretOffset >= 0 && caretOffset <= len, "co: " + caretOffset + " doc: " + len);
hostEditor.getDocument().replaceString(lookupStart, caretOffset, lookupString);
int offset = lookupStart + lookupString.length();
hostEditor.getCaretModel().moveToOffset(offset);
hostEditor.getSelectionModel().removeSelection();
}
});
}
myEditor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
private String getCaseCorrectedLookupString(LookupElement item) {
String lookupString = item.getLookupString();
if (item.isCaseSensitive()) {
return lookupString;
}
final String prefix = itemPattern(item);
final int length = prefix.length();
if (length == 0 || !itemMatcher(item).prefixMatches(prefix)) return lookupString;
boolean isAllLower = true;
boolean isAllUpper = true;
boolean sameCase = true;
for (int i = 0; i < length && (isAllLower || isAllUpper || sameCase); i++) {
final char c = prefix.charAt(i);
boolean isLower = Character.isLowerCase(c);
boolean isUpper = Character.isUpperCase(c);
// do not take this kind of symbols into account ('_', '@', etc.)
if (!isLower && !isUpper) continue;
isAllLower = isAllLower && isLower;
isAllUpper = isAllUpper && isUpper;
sameCase = sameCase && isLower == Character.isLowerCase(lookupString.charAt(i));
}
if (sameCase) return lookupString;
if (isAllLower) return lookupString.toLowerCase();
if (isAllUpper) return StringUtil.toUpperCase(lookupString);
return lookupString;
}
@Override
public int getLookupStart() {
return myOffsets.getLookupStart(disposeTrace);
}
public int getLookupOriginalStart() {
return myOffsets.getLookupOriginalStart();
}
public boolean performGuardedChange(Runnable change) {
checkValid();
assert !myChangeGuard : "already in change";
myEditor.getDocument().startGuardedBlockChecking();
myChangeGuard = true;
boolean result;
try {
result = myOffsets.performGuardedChange(change);
}
finally {
myEditor.getDocument().stopGuardedBlockChecking();
myChangeGuard = false;
}
if (!result || myDisposed) {
hide();
return false;
}
if (isVisible()) {
HintManagerImpl.updateLocation(this, myEditor, myUi.calculatePosition().getLocation());
}
checkValid();
return true;
}
@Override
public boolean vetoesHiding() {
return myChangeGuard;
}
public boolean isAvailableToUser() {
if (ApplicationManager.getApplication().isUnitTestMode()) {
return myShown;
}
return isVisible();
}
public boolean isShown() {
if (!ApplicationManager.getApplication().isUnitTestMode()) {
ApplicationManager.getApplication().assertIsDispatchThread();
}
return myShown;
}
public boolean showLookup() {
ApplicationManager.getApplication().assertIsDispatchThread();
checkValid();
LOG.assertTrue(!myShown);
myShown = true;
myStampShown = System.currentTimeMillis();
if (ApplicationManager.getApplication().isUnitTestMode()) return true;
if (!myEditor.getContentComponent().isShowing()) {
hide();
return false;
}
myAdComponent.showRandomText();
myUi = new LookupUi(this, myAdComponent, myList, myProject);
myUi.setCalculating(myCalculating);
Point p = myUi.calculatePosition().getLocation();
HintManagerImpl.getInstanceImpl().showEditorHint(this, myEditor, p, HintManager.HIDE_BY_ESCAPE | HintManager.UPDATE_BY_SCROLLING, 0, false,
HintManagerImpl.createHintHint(myEditor, p, this, HintManager.UNDER).setAwtTooltip(false));
if (!isVisible()) {
hide();
return false;
}
DaemonCodeAnalyzer.getInstance(myProject).disableUpdateByTimer(this);
LOG.assertTrue(myList.isShowing(), "!showing, disposed=" + myDisposed);
return true;
}
public Advertiser getAdvertiser() {
return myAdComponent;
}
public boolean mayBeNoticed() {
return myStampShown > 0 && System.currentTimeMillis() - myStampShown > 300;
}
private void addListeners() {
myEditor.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
public void documentChanged(DocumentEvent e) {
if (!myChangeGuard && !myFinishing) {
hide();
}
}
}, this);
final CaretListener caretListener = new CaretAdapter() {
@Override
public void caretPositionChanged(CaretEvent e) {
if (!myChangeGuard && !myFinishing) {
hide();
}
}
};
final SelectionListener selectionListener = new SelectionListener() {
@Override
public void selectionChanged(final SelectionEvent e) {
if (!myChangeGuard && !myFinishing) {
hide();
}
}
};
final EditorMouseListener mouseListener = new EditorMouseAdapter() {
@Override
public void mouseClicked(EditorMouseEvent e){
e.consume();
hide();
}
};
myEditor.getCaretModel().addCaretListener(caretListener);
myEditor.getSelectionModel().addSelectionListener(selectionListener);
myEditor.addEditorMouseListener(mouseListener);
Disposer.register(this, new Disposable() {
@Override
public void dispose() {
myEditor.getCaretModel().removeCaretListener(caretListener);
myEditor.getSelectionModel().removeSelectionListener(selectionListener);
myEditor.removeEditorMouseListener(mouseListener);
}
});
JComponent editorComponent = myEditor.getContentComponent();
if (editorComponent.isShowing()) {
Disposer.register(this, new UiNotifyConnector(editorComponent, new Activatable() {
@Override
public void showNotify() {
}
@Override
public void hideNotify() {
hideLookup(false);
}
}));
}
myList.addListSelectionListener(new ListSelectionListener() {
private LookupElement oldItem = null;
@Override
public void valueChanged(@NotNull ListSelectionEvent e){
if (!myUpdating) {
final LookupElement item = getCurrentItem();
fireCurrentItemChanged(oldItem, item);
oldItem = item;
}
}
});
new ClickListener() {
@Override
public boolean onClick(@NotNull MouseEvent e, int clickCount) {
setFocusDegree(FocusDegree.FOCUSED);
markSelectionTouched();
if (clickCount == 2){
CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
@Override
public void run() {
finishLookup(NORMAL_SELECT_CHAR);
}
}, "", null);
}
return true;
}
}.installOn(myList);
}
@Override
@Nullable
public LookupElement getCurrentItem(){
LookupElement item = (LookupElement)myList.getSelectedValue();
return item instanceof EmptyLookupItem ? null : item;
}
@Override
public void setCurrentItem(LookupElement item){
markSelectionTouched();
myList.setSelectedValue(item, false);
}
@Override
public void addLookupListener(LookupListener listener){
myListeners.add(listener);
}
@Override
public void removeLookupListener(LookupListener listener){
myListeners.remove(listener);
}
@Override
public Rectangle getCurrentItemBounds(){
int index = myList.getSelectedIndex();
if (index < 0) {
LOG.error("No selected element, size=" + getListModel().getSize() + "; items" + getItems());
}
Rectangle itmBounds = myList.getCellBounds(index, index);
if (itmBounds == null){
LOG.error("No bounds for " + index + "; size=" + getListModel().getSize());
return null;
}
Point layeredPanePoint=SwingUtilities.convertPoint(myList,itmBounds.x,itmBounds.y,getComponent());
itmBounds.x = layeredPanePoint.x;
itmBounds.y = layeredPanePoint.y;
return itmBounds;
}
public void fireItemSelected(@Nullable final LookupElement item, char completionChar){
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
if (!myListeners.isEmpty()){
LookupEvent event = new LookupEvent(this, item, completionChar);
for (LookupListener listener : myListeners) {
try {
listener.itemSelected(event);
}
catch (Throwable e) {
LOG.error(e);
}
}
}
}
private void fireLookupCanceled(final boolean explicitly) {
if (!myListeners.isEmpty()){
LookupEvent event = new LookupEvent(this, explicitly);
for (LookupListener listener : myListeners) {
try {
listener.lookupCanceled(event);
}
catch (Throwable e) {
LOG.error(e);
}
}
}
}
private void fireCurrentItemChanged(@Nullable LookupElement oldItem, @Nullable LookupElement currentItem) {
if (oldItem != currentItem && !myListeners.isEmpty()) {
LookupEvent event = new LookupEvent(this, currentItem, (char)0);
for (LookupListener listener : myListeners) {
listener.currentItemChanged(event);
}
}
}
public boolean fillInCommonPrefix(boolean explicitlyInvoked) {
if (explicitlyInvoked) {
setFocusDegree(FocusDegree.FOCUSED);
}
if (explicitlyInvoked && myCalculating) return false;
if (!explicitlyInvoked && mySelectionTouched) return false;
ListModel listModel = getListModel();
if (listModel.getSize() <= 1) return false;
if (listModel.getSize() == 0) return false;
final LookupElement firstItem = (LookupElement)listModel.getElementAt(0);
if (listModel.getSize() == 1 && firstItem instanceof EmptyLookupItem) return false;
final PrefixMatcher firstItemMatcher = itemMatcher(firstItem);
final String oldPrefix = firstItemMatcher.getPrefix();
final String presentPrefix = oldPrefix + getAdditionalPrefix();
String commonPrefix = getCaseCorrectedLookupString(firstItem);
for (int i = 1; i < listModel.getSize(); i++) {
LookupElement item = (LookupElement)listModel.getElementAt(i);
if (item instanceof EmptyLookupItem) return false;
if (!oldPrefix.equals(itemMatcher(item).getPrefix())) return false;
final String lookupString = getCaseCorrectedLookupString(item);
final int length = Math.min(commonPrefix.length(), lookupString.length());
if (length < commonPrefix.length()) {
commonPrefix = commonPrefix.substring(0, length);
}
for (int j = 0; j < length; j++) {
if (commonPrefix.charAt(j) != lookupString.charAt(j)) {
commonPrefix = lookupString.substring(0, j);
break;
}
}
if (commonPrefix.length() == 0 || commonPrefix.length() < presentPrefix.length()) {
return false;
}
}
if (commonPrefix.equals(presentPrefix)) {
return false;
}
for (int i = 0; i < listModel.getSize(); i++) {
LookupElement item = (LookupElement)listModel.getElementAt(i);
if (!itemMatcher(item).cloneWithPrefix(commonPrefix).prefixMatches(item)) {
return false;
}
}
myOffsets.setInitialPrefix(presentPrefix, explicitlyInvoked);
replacePrefix(presentPrefix, commonPrefix);
return true;
}
public void replacePrefix(final String presentPrefix, final String newPrefix) {
if (!performGuardedChange(new Runnable() {
@Override
public void run() {
EditorModificationUtil.deleteSelectedText(myEditor);
int offset = myEditor.getCaretModel().getOffset();
final int start = offset - presentPrefix.length();
myEditor.getDocument().replaceString(start, offset, newPrefix);
Map<LookupElement, PrefixMatcher> newMatchers = new HashMap<LookupElement, PrefixMatcher>();
for (LookupElement item : getItems()) {
if (item.isValid()) {
PrefixMatcher matcher = itemMatcher(item).cloneWithPrefix(newPrefix);
if (matcher.prefixMatches(item)) {
newMatchers.put(item, matcher);
}
}
}
myMatchers.clear();
myMatchers.putAll(newMatchers);
myOffsets.clearAdditionalPrefix();
myEditor.getCaretModel().moveToOffset(start + newPrefix.length());
}
})) {
return;
}
synchronized (myList) {
myPresentableArranger.prefixChanged(this);
}
refreshUi(true, true);
}
@Override
@Nullable
public PsiFile getPsiFile() {
return PsiDocumentManager.getInstance(myProject).getPsiFile(myEditor.getDocument());
}
@Override
public boolean isCompletion() {
return myArranger instanceof CompletionLookupArranger;
}
@Override
public PsiElement getPsiElement() {
PsiFile file = getPsiFile();
if (file == null) return null;
int offset = getLookupStart();
if (offset > 0) return file.findElementAt(offset - 1);
return file.findElementAt(0);
}
@Override
public Editor getEditor() {
return myEditor;
}
@Override
public boolean isPositionedAboveCaret(){
return myUi != null && myUi.isPositionedAboveCaret();
}
@Override
public boolean isSelectionTouched() {
return mySelectionTouched;
}
@Override
public List<String> getAdvertisements() {
return myAdComponent.getAdvertisements();
}
@Override
public void hide(){
hideLookup(true);
}
public void hideLookup(boolean explicitly) {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myHidden) return;
doHide(true, explicitly);
}
private void doHide(final boolean fireCanceled, final boolean explicitly) {
if (myDisposed) {
LOG.error(disposeTrace);
}
else {
myHidden = true;
try {
super.hide();
Disposer.dispose(this);
assert myDisposed;
}
catch (Throwable e) {
LOG.error(e);
}
}
if (fireCanceled) {
fireLookupCanceled(explicitly);
}
}
public void restorePrefix() {
myOffsets.restorePrefix(getLookupStart());
}
private static String staticDisposeTrace = null;
private String disposeTrace = null;
public static String getLastLookupDisposeTrace() {
return staticDisposeTrace;
}
@Override
public void dispose() {
assert ApplicationManager.getApplication().isDispatchThread();
assert myHidden;
if (myDisposed) {
LOG.error(disposeTrace);
return;
}
myOffsets.disposeMarkers();
myDisposed = true;
disposeTrace = DebugUtil.currentStackTrace() + "\n============";
//noinspection AssignmentToStaticFieldFromInstanceMethod
staticDisposeTrace = disposeTrace;
}
public void refreshUi(boolean mayCheckReused, boolean onExplicitAction) {
assert !myUpdating;
LookupElement prevItem = getCurrentItem();
myUpdating = true;
try {
final boolean reused = mayCheckReused && checkReused();
boolean selectionVisible = isSelectionVisible();
boolean itemsChanged = updateList(onExplicitAction, reused);
if (isVisible()) {
LOG.assertTrue(!ApplicationManager.getApplication().isUnitTestMode());
myUi.refreshUi(selectionVisible, itemsChanged, reused, onExplicitAction);
}
}
finally {
myUpdating = false;
fireCurrentItemChanged(prevItem, getCurrentItem());
}
}
public void markReused() {
synchronized (myList) {
myArranger = myArranger.createEmptyCopy();
}
requestResize();
}
public void addAdvertisement(@NotNull final String text, final @Nullable Color bgColor) {
if (containsDummyIdentifier(text)) {
return;
}
myAdComponent.addAdvertisement(text, bgColor);
requestResize();
}
public boolean isLookupDisposed() {
return myDisposed;
}
public void checkValid() {
if (myDisposed) {
throw new AssertionError("Disposed at: " + disposeTrace);
}
}
@Override
public void showItemPopup(JBPopup hint) {
final Rectangle bounds = getCurrentItemBounds();
hint.show(new RelativePoint(getComponent(), new Point(bounds.x + bounds.width, bounds.y)));
}
@Override
public boolean showElementActions() {
if (!isVisible()) return false;
final LookupElement element = getCurrentItem();
if (element == null) {
return false;
}
final Collection<LookupElementAction> actions = getActionsFor(element);
if (actions.isEmpty()) {
return false;
}
showItemPopup(JBPopupFactory.getInstance().createListPopup(new LookupActionsStep(actions, this, element)));
return true;
}
public Map<LookupElement,StringBuilder> getRelevanceStrings() {
synchronized (myList) {
return myPresentableArranger.getRelevanceStrings();
}
}
public enum FocusDegree { FOCUSED, SEMI_FOCUSED, UNFOCUSED }
}