Merge branch 'master' of git.labs.intellij.net:idea/community
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / completion / CompletionProgressIndicator.java
1 /*
2  * Copyright 2000-2014 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.completion;
18
19 import com.intellij.codeInsight.CodeInsightSettings;
20 import com.intellij.codeInsight.TargetElementUtilBase;
21 import com.intellij.codeInsight.completion.impl.CompletionServiceImpl;
22 import com.intellij.codeInsight.completion.impl.CompletionSorterImpl;
23 import com.intellij.codeInsight.editorActions.CompletionAutoPopupHandler;
24 import com.intellij.codeInsight.hint.EditorHintListener;
25 import com.intellij.codeInsight.hint.HintManager;
26 import com.intellij.codeInsight.lookup.*;
27 import com.intellij.codeInsight.lookup.impl.LookupImpl;
28 import com.intellij.diagnostic.PerformanceWatcher;
29 import com.intellij.featureStatistics.FeatureUsageTracker;
30 import com.intellij.injected.editor.DocumentWindow;
31 import com.intellij.injected.editor.EditorWindow;
32 import com.intellij.lang.Language;
33 import com.intellij.openapi.Disposable;
34 import com.intellij.openapi.actionSystem.IdeActions;
35 import com.intellij.openapi.application.AccessToken;
36 import com.intellij.openapi.application.ApplicationManager;
37 import com.intellij.openapi.application.Result;
38 import com.intellij.openapi.application.WriteAction;
39 import com.intellij.openapi.command.CommandProcessor;
40 import com.intellij.openapi.command.WriteCommandAction;
41 import com.intellij.openapi.diagnostic.Logger;
42 import com.intellij.openapi.editor.Caret;
43 import com.intellij.openapi.editor.Editor;
44 import com.intellij.openapi.extensions.Extensions;
45 import com.intellij.openapi.progress.ProcessCanceledException;
46 import com.intellij.openapi.progress.ProgressManager;
47 import com.intellij.openapi.progress.util.ProgressIndicatorBase;
48 import com.intellij.openapi.progress.util.ProgressWrapper;
49 import com.intellij.openapi.project.DumbService;
50 import com.intellij.openapi.project.IndexNotReadyException;
51 import com.intellij.openapi.project.Project;
52 import com.intellij.openapi.ui.MessageType;
53 import com.intellij.openapi.util.Disposer;
54 import com.intellij.openapi.util.Pair;
55 import com.intellij.openapi.util.TextRange;
56 import com.intellij.openapi.util.registry.Registry;
57 import com.intellij.openapi.util.text.StringUtil;
58 import com.intellij.patterns.ElementPattern;
59 import com.intellij.psi.PsiFile;
60 import com.intellij.psi.PsiReference;
61 import com.intellij.psi.ReferenceRange;
62 import com.intellij.psi.util.PsiUtilCore;
63 import com.intellij.ui.LightweightHint;
64 import com.intellij.util.Alarm;
65 import com.intellij.util.ObjectUtils;
66 import com.intellij.util.ThreeState;
67 import com.intellij.util.concurrency.Semaphore;
68 import com.intellij.util.containers.ContainerUtil;
69 import com.intellij.util.messages.MessageBusConnection;
70 import com.intellij.util.ui.update.MergingUpdateQueue;
71 import com.intellij.util.ui.update.Update;
72 import org.jetbrains.annotations.NotNull;
73 import org.jetbrains.annotations.Nullable;
74 import org.jetbrains.annotations.TestOnly;
75
76 import javax.swing.*;
77 import java.awt.*;
78 import java.awt.event.KeyAdapter;
79 import java.awt.event.KeyEvent;
80 import java.beans.PropertyChangeEvent;
81 import java.beans.PropertyChangeListener;
82 import java.util.List;
83 import java.util.Queue;
84 import java.util.concurrent.ConcurrentLinkedQueue;
85 import java.util.concurrent.ConcurrentMap;
86
87 /**
88  * @author peter
89  */
90 public class CompletionProgressIndicator extends ProgressIndicatorBase implements CompletionProcess, Disposable {
91   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.CompletionProgressIndicator");
92   private final Editor myEditor;
93   @NotNull
94   private final Caret myCaret;
95   private final CompletionParameters myParameters;
96   private final CodeCompletionHandlerBase myHandler;
97   private final LookupImpl myLookup;
98   private final MergingUpdateQueue myQueue;
99   private final Update myUpdate = new Update("update") {
100     @Override
101     public void run() {
102       updateLookup();
103       myQueue.setMergingTimeSpan(300);
104     }
105   };
106   private final Semaphore myFreezeSemaphore;
107   private final OffsetMap myOffsetMap;
108   private final List<Pair<Integer, ElementPattern<String>>> myRestartingPrefixConditions = ContainerUtil.createLockFreeCopyOnWriteList();
109   private final LookupAdapter myLookupListener = new LookupAdapter() {
110     @Override
111     public void itemSelected(LookupEvent event) {
112       finishCompletionProcess(false);
113
114       LookupElement item = event.getItem();
115       if (item == null) return;
116
117       setMergeCommand();
118
119       myHandler.lookupItemSelected(CompletionProgressIndicator.this, item, event.getCompletionChar(), myLookup.getItems());
120     }
121
122
123     @Override
124     public void lookupCanceled(final LookupEvent event) {
125       finishCompletionProcess(true);
126     }
127   };
128   private volatile int myCount;
129   private volatile boolean myHasPsiElements;
130   private boolean myLookupUpdated;
131   private final ConcurrentMap<LookupElement, CompletionSorterImpl> myItemSorters =
132     ContainerUtil.newConcurrentMap(ContainerUtil.<LookupElement>identityStrategy());
133   private final PropertyChangeListener myLookupManagerListener;
134   private final Queue<Runnable> myAdvertiserChanges = new ConcurrentLinkedQueue<Runnable>();
135   private final int myStartCaret;
136
137   public CompletionProgressIndicator(final Editor editor,
138                                      @NotNull Caret caret,
139                                      CompletionParameters parameters,
140                                      CodeCompletionHandlerBase handler,
141                                      Semaphore freezeSemaphore,
142                                      final OffsetMap offsetMap,
143                                      boolean hasModifiers,
144                                      LookupImpl lookup) {
145     myEditor = editor;
146     myCaret = caret;
147     myParameters = parameters;
148     myHandler = handler;
149     myFreezeSemaphore = freezeSemaphore;
150     myOffsetMap = offsetMap;
151     myLookup = lookup;
152     myStartCaret = myEditor.getCaretModel().getOffset();
153
154     myAdvertiserChanges.offer(new Runnable() {
155       @Override
156       public void run() {
157         myLookup.getAdvertiser().clearAdvertisements();
158       }
159     });
160
161     myLookup.setArranger(new CompletionLookupArranger(parameters, this));
162
163     myLookup.addLookupListener(myLookupListener);
164     myLookup.setCalculating(true);
165
166     myLookupManagerListener = new PropertyChangeListener() {
167       @Override
168       public void propertyChange(PropertyChangeEvent evt) {
169         if (evt.getNewValue() != null) {
170           LOG.error("An attempt to change the lookup during completion, phase = " + CompletionServiceImpl.getCompletionPhase());
171         }
172       }
173     };
174     LookupManager.getInstance(getProject()).addPropertyChangeListener(myLookupManagerListener);
175
176     myQueue = new MergingUpdateQueue("completion lookup progress", 100, true, myEditor.getContentComponent());
177     myQueue.setPassThrough(false);
178
179     ApplicationManager.getApplication().assertIsDispatchThread();
180     Disposer.register(this, offsetMap);
181
182     if (hasModifiers && !ApplicationManager.getApplication().isUnitTestMode()) {
183       trackModifiers();
184     }
185   }
186
187   public OffsetMap getOffsetMap() {
188     return myOffsetMap;
189   }
190
191   public int getSelectionEndOffset() {
192     return getOffsetMap().getOffset(CompletionInitializationContext.SELECTION_END_OFFSET);
193   }
194
195   void duringCompletion(CompletionInitializationContext initContext) {
196     if (isAutopopupCompletion()) {
197       if (shouldPreselectFirstSuggestion(myParameters)) {
198         if (!CodeInsightSettings.getInstance().SELECT_AUTOPOPUP_SUGGESTIONS_BY_CHARS) {
199           myLookup.setFocusDegree(LookupImpl.FocusDegree.SEMI_FOCUSED);
200           if (FeatureUsageTracker.getInstance().isToBeAdvertisedInLookup(CodeCompletionFeatures.EDITING_COMPLETION_FINISH_BY_CONTROL_DOT, getProject())) {
201             String dotShortcut = CompletionContributor.getActionShortcut(IdeActions.ACTION_CHOOSE_LOOKUP_ITEM_DOT);
202             if (StringUtil.isNotEmpty(dotShortcut)) {
203               addAdvertisement("Press " + dotShortcut + " to choose the selected (or first) suggestion and insert a dot afterwards", null);
204             }
205           }
206         } else {
207           myLookup.setFocusDegree(LookupImpl.FocusDegree.FOCUSED);
208         }
209       }
210       if (!myEditor.isOneLineMode() &&
211           FeatureUsageTracker.getInstance()
212             .isToBeAdvertisedInLookup(CodeCompletionFeatures.EDITING_COMPLETION_CONTROL_ARROWS, getProject())) {
213         String downShortcut = CompletionContributor.getActionShortcut(IdeActions.ACTION_LOOKUP_DOWN);
214         String upShortcut = CompletionContributor.getActionShortcut(IdeActions.ACTION_LOOKUP_UP);
215         if (StringUtil.isNotEmpty(downShortcut) && StringUtil.isNotEmpty(upShortcut)) {
216           addAdvertisement(downShortcut + " and " + upShortcut + " will move caret down and up in the editor", null);
217         }
218       }
219     } else if (DumbService.isDumb(getProject())) {
220       addAdvertisement("The results might be incomplete while indexing is in progress", MessageType.WARNING.getPopupBackground());
221     }
222
223     ProgressManager.checkCanceled();
224
225     if (!initContext.getOffsetMap().wasModified(CompletionInitializationContext.IDENTIFIER_END_OFFSET)) {
226       try {
227         final int selectionEndOffset = initContext.getSelectionEndOffset();
228         final PsiReference reference = TargetElementUtilBase.findReference(myEditor, selectionEndOffset);
229         if (reference != null) {
230           initContext.setReplacementOffset(findReplacementOffset(selectionEndOffset, reference));
231         }
232       }
233       catch (IndexNotReadyException ignored) {
234       }
235     }
236
237     for (CompletionContributor contributor : CompletionContributor.forLanguage(initContext.getPositionLanguage())) {
238       ProgressManager.checkCanceled();
239       if (DumbService.getInstance(initContext.getProject()).isDumb() && !DumbService.isDumbAware(contributor)) {
240         continue;
241       }
242
243       contributor.duringCompletion(initContext);
244     }
245   }
246
247   @NotNull
248   CompletionSorterImpl getSorter(LookupElement element) {
249     return myItemSorters.get(element);
250   }
251
252   @Override
253   public void dispose() {
254   }
255
256   private static int findReplacementOffset(int selectionEndOffset, PsiReference reference) {
257     final List<TextRange> ranges = ReferenceRange.getAbsoluteRanges(reference);
258     for (TextRange range : ranges) {
259       if (range.contains(selectionEndOffset)) {
260         return range.getEndOffset();
261       }
262     }
263
264     return selectionEndOffset;
265   }
266
267
268   void scheduleAdvertising() {
269     if (myLookup.isAvailableToUser()) {
270       return;
271     }
272     for (final CompletionContributor contributor : CompletionContributor.forParameters(myParameters)) {
273       if (!myLookup.isCalculating() && !myLookup.isVisible()) return;
274
275       @SuppressWarnings("deprecation") String s = contributor.advertise(myParameters);
276       if (s != null) {
277         addAdvertisement(s, null);
278       }
279     }
280   }
281
282   private boolean isOutdated() {
283     return CompletionServiceImpl.getCompletionPhase().indicator != this;
284   }
285
286   private void trackModifiers() {
287     assert !isAutopopupCompletion();
288
289     final JComponent contentComponent = myEditor.getContentComponent();
290     contentComponent.addKeyListener(new ModifierTracker(contentComponent));
291   }
292
293   public void setMergeCommand() {
294     CommandProcessor.getInstance().setCurrentCommandGroupId(getCompletionCommandName());
295   }
296
297   private String getCompletionCommandName() {
298     return "Completion" + hashCode();
299   }
300
301   public boolean showLookup() {
302     return updateLookup();
303   }
304
305   public CompletionParameters getParameters() {
306     return myParameters;
307   }
308
309   public CodeCompletionHandlerBase getHandler() {
310     return myHandler;
311   }
312
313   public LookupImpl getLookup() {
314     return myLookup;
315   }
316
317   private boolean updateLookup() {
318     ApplicationManager.getApplication().assertIsDispatchThread();
319     if (isOutdated() || !shouldShowLookup()) return false;
320
321     while (true) {
322       Runnable action = myAdvertiserChanges.poll();
323       if (action == null) break;
324       action.run();
325     }
326
327     if (!myLookupUpdated) {
328       if (myLookup.getAdvertisements().isEmpty() && !isAutopopupCompletion() && !DumbService.isDumb(getProject())) {
329         DefaultCompletionContributor.addDefaultAdvertisements(myParameters, myLookup, myHasPsiElements);
330       }
331       myLookup.getAdvertiser().showRandomText();
332     }
333
334     boolean justShown = false;
335     if (!myLookup.isShown()) {
336       if (hideAutopopupIfMeaningless()) {
337         return false;
338       }
339
340       if (Registry.is("dump.threads.on.empty.lookup") && myLookup.isCalculating() && myLookup.getItems().isEmpty()) {
341         PerformanceWatcher.getInstance().dumpThreads("emptyLookup/", true);
342       }
343
344       if (!myLookup.showLookup()) {
345         return false;
346       }
347       justShown = true;
348     }
349     myLookupUpdated = true;
350     myLookup.refreshUi(true, justShown);
351     hideAutopopupIfMeaningless();
352     if (justShown) {
353       myLookup.ensureSelectionVisible(true);
354     }
355     return true;
356   }
357
358   private boolean shouldShowLookup() {
359     if (isAutopopupCompletion()) {
360       if (myCount == 0) {
361         return false;
362       }
363       if (myLookup.isCalculating() && Registry.is("ide.completion.delay.autopopup.until.completed")) {
364         return false;
365       }
366     }
367     return true;
368   }
369
370   final boolean isInsideIdentifier() {
371     return getIdentifierEndOffset() != getSelectionEndOffset();
372   }
373
374   public int getIdentifierEndOffset() {
375     return myOffsetMap.getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET);
376   }
377
378   public synchronized void addItem(final CompletionResult item) {
379     if (!isRunning()) return;
380     ProgressManager.checkCanceled();
381
382     final boolean unitTestMode = ApplicationManager.getApplication().isUnitTestMode();
383     if (!unitTestMode) {
384       LOG.assertTrue(!ApplicationManager.getApplication().isDispatchThread());
385     }
386
387     LOG.assertTrue(myParameters.getPosition().isValid());
388
389     LookupElement lookupElement = item.getLookupElement();
390     if (!myHasPsiElements && lookupElement.getPsiElement() != null) {
391       myHasPsiElements = true;
392     }
393     myItemSorters.put(lookupElement, (CompletionSorterImpl)item.getSorter());
394     if (!myLookup.addItem(lookupElement, item.getPrefixMatcher())) {
395       return;
396     }
397     myCount++;
398
399     if (myCount == 1) {
400       new Alarm(Alarm.ThreadToUse.SHARED_THREAD, this).addRequest(new Runnable() {
401         @Override
402         public void run() {
403           myFreezeSemaphore.up();
404         }
405       }, 300);
406     }
407     myQueue.queue(myUpdate);
408   }
409
410   public void closeAndFinish(boolean hideLookup) {
411     if (!myLookup.isLookupDisposed()) {
412       Lookup lookup = LookupManager.getActiveLookup(myEditor);
413       LOG.assertTrue(lookup == myLookup, "lookup changed: " + lookup + "; " + this);
414     }
415     myLookup.removeLookupListener(myLookupListener);
416     finishCompletionProcess(true);
417     CompletionServiceImpl.assertPhase(CompletionPhase.NoCompletion.getClass());
418
419     if (hideLookup) {
420       LookupManager.getInstance(getProject()).hideActiveLookup();
421     }
422   }
423
424   private void finishCompletionProcess(boolean disposeOffsetMap) {
425     cancel();
426
427     ApplicationManager.getApplication().assertIsDispatchThread();
428     Disposer.dispose(myQueue);
429     LookupManager.getInstance(getProject()).removePropertyChangeListener(myLookupManagerListener);
430
431     CompletionProgressIndicator currentCompletion = CompletionServiceImpl.getCompletionService().getCurrentCompletion();
432     LOG.assertTrue(currentCompletion == this, currentCompletion + "!=" + this);
433
434     CompletionServiceImpl
435       .assertPhase(CompletionPhase.BgCalculation.class, CompletionPhase.ItemsCalculated.class, CompletionPhase.Synchronous.class,
436                    CompletionPhase.CommittingDocuments.class);
437     CompletionPhase oldPhase = CompletionServiceImpl.getCompletionPhase();
438     if (oldPhase instanceof CompletionPhase.CommittingDocuments) {
439       LOG.assertTrue(((CompletionPhase.CommittingDocuments)oldPhase).isRestartingCompletion(), oldPhase);
440       ((CompletionPhase.CommittingDocuments)oldPhase).replaced = true;
441     }
442     CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion);
443     if (disposeOffsetMap) {
444       disposeIndicator();
445     }
446   }
447
448   void disposeIndicator() {
449     // our offset map should be disposed under write action, so that duringCompletion (read action) won't access it after disposing
450     AccessToken token = WriteAction.start();
451     try {
452       Disposer.dispose(this);
453     }
454     finally {
455       token.finish();
456     }
457   }
458
459   @TestOnly
460   public static void cleanupForNextTest() {
461     CompletionProgressIndicator currentCompletion = CompletionServiceImpl.getCompletionService().getCurrentCompletion();
462     if (currentCompletion != null) {
463       currentCompletion.finishCompletionProcess(true);
464       CompletionServiceImpl.assertPhase(CompletionPhase.NoCompletion.getClass());
465     }
466     else {
467       CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion);
468     }
469     CompletionLookupArranger.cancelLastCompletionStatisticsUpdate();
470   }
471
472   @Override
473   public void stop() {
474     super.stop();
475
476     myQueue.cancelAllUpdates();
477     myFreezeSemaphore.up();
478
479     ApplicationManager.getApplication().invokeLater(new Runnable() {
480       @Override
481       public void run() {
482         final CompletionPhase phase = CompletionServiceImpl.getCompletionPhase();
483         if (!(phase instanceof CompletionPhase.BgCalculation) || phase.indicator != CompletionProgressIndicator.this) return;
484
485         LOG.assertTrue(!getProject().isDisposed(), "project disposed");
486
487         if (myEditor.isDisposed()) {
488           LookupManager.getInstance(getProject()).hideActiveLookup();
489           CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion);
490           return;
491         }
492
493         if (myEditor instanceof EditorWindow) {
494           LOG.assertTrue(((EditorWindow)myEditor).getInjectedFile().isValid(), "injected file !valid");
495           LOG.assertTrue(((DocumentWindow)myEditor.getDocument()).isValid(), "docWindow !valid");
496         }
497         PsiFile file = myLookup.getPsiFile();
498         LOG.assertTrue(file == null || file.isValid(), "file !valid");
499
500         myLookup.setCalculating(false);
501
502         if (myCount == 0) {
503           LookupManager.getInstance(getProject()).hideActiveLookup();
504           if (!isAutopopupCompletion()) {
505             final CompletionProgressIndicator current = CompletionServiceImpl.getCompletionService().getCurrentCompletion();
506             LOG.assertTrue(current == null, current + "!=" + CompletionProgressIndicator.this);
507
508             handleEmptyLookup(!((CompletionPhase.BgCalculation)phase).modifiersChanged);
509           }
510         }
511         else {
512           CompletionServiceImpl.setCompletionPhase(new CompletionPhase.ItemsCalculated(CompletionProgressIndicator.this));
513           updateLookup();
514         }
515       }
516     }, myQueue.getModalityState());
517   }
518
519   private boolean hideAutopopupIfMeaningless() {
520     if (!myLookup.isLookupDisposed() && isAutopopupCompletion() && !myLookup.isSelectionTouched() && !myLookup.isCalculating()) {
521       myLookup.refreshUi(true, false);
522       final List<LookupElement> items = myLookup.getItems();
523
524       for (LookupElement item : items) {
525         if (!myLookup.itemPattern(item).equals(item.getLookupString())) {
526           return false;
527         }
528
529         if (item.isValid() && item.isWorthShowingInAutoPopup()) {
530           return false;
531         }
532       }
533
534       myLookup.hideLookup(false);
535       LOG.assertTrue(CompletionServiceImpl.getCompletionService().getCurrentCompletion() == null);
536       CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion);
537       return true;
538     }
539     return false;
540   }
541
542   public boolean fillInCommonPrefix(final boolean explicit) {
543     if (isInsideIdentifier()) {
544       return false;
545     }
546
547     final Boolean aBoolean = new WriteCommandAction<Boolean>(getProject()) {
548       @Override
549       protected void run(@NotNull Result<Boolean> result) throws Throwable {
550         if (!explicit) {
551           setMergeCommand();
552         }
553         try {
554           result.setResult(myLookup.fillInCommonPrefix(explicit));
555         }
556         catch (Exception e) {
557           LOG.error(e);
558         }
559       }
560     }.execute().getResultObject();
561     return aBoolean.booleanValue();
562   }
563
564   public void restorePrefix(@NotNull final Runnable customRestore) {
565     new WriteCommandAction(getProject()) {
566       @Override
567       protected void run(@NotNull Result result) throws Throwable {
568         setMergeCommand();
569
570         customRestore.run();
571       }
572     }.execute();
573   }
574
575   public int nextInvocationCount(int invocation, boolean reused) {
576     return reused ? Math.max(getParameters().getInvocationCount() + 1, 2) : invocation;
577   }
578
579   public Editor getEditor() {
580     return myEditor;
581   }
582
583   @NotNull
584   public Caret getCaret() {
585     return myCaret;
586   }
587
588   public boolean isRepeatedInvocation(CompletionType completionType, Editor editor) {
589     if (completionType != myParameters.getCompletionType() || editor != myEditor) {
590       return false;
591     }
592
593     if (isAutopopupCompletion() && !myLookup.mayBeNoticed()) {
594       return false;
595     }
596
597     return true;
598   }
599
600   @Override
601   public boolean isAutopopupCompletion() {
602     return myParameters.getInvocationCount() == 0;
603   }
604
605   @NotNull
606   public Project getProject() {
607     return ObjectUtils.assertNotNull(myEditor.getProject());
608   }
609
610   public void addWatchedPrefix(int startOffset, ElementPattern<String> restartCondition) {
611     myRestartingPrefixConditions.add(Pair.create(startOffset, restartCondition));
612   }
613
614   public void prefixUpdated() {
615     final int caretOffset = myEditor.getCaretModel().getOffset();
616     if (caretOffset < myStartCaret) {
617       scheduleRestart();
618       myRestartingPrefixConditions.clear();
619       return;
620     }
621
622     final CharSequence text = myEditor.getDocument().getCharsSequence();
623     for (Pair<Integer, ElementPattern<String>> pair : myRestartingPrefixConditions) {
624       int start = pair.first;
625       if (caretOffset >= start && start >= 0) {
626         final String newPrefix = text.subSequence(start, caretOffset).toString();
627         if (pair.second.accepts(newPrefix)) {
628           scheduleRestart();
629           myRestartingPrefixConditions.clear();
630           return;
631         }
632       }
633     }
634
635     hideAutopopupIfMeaningless();
636   }
637
638   public void scheduleRestart() {
639     ApplicationManager.getApplication().assertIsDispatchThread();
640     cancel();
641
642     final CompletionProgressIndicator current = CompletionServiceImpl.getCompletionService().getCurrentCompletion();
643     if (this != current) {
644       LOG.error(current + "!=" + this);
645     }
646
647     hideAutopopupIfMeaningless();
648
649     CompletionPhase oldPhase = CompletionServiceImpl.getCompletionPhase();
650     if (oldPhase instanceof CompletionPhase.CommittingDocuments) {
651       ((CompletionPhase.CommittingDocuments)oldPhase).replaced = true;
652     }
653
654     final CompletionPhase.CommittingDocuments phase = new CompletionPhase.CommittingDocuments(this, myEditor);
655     CompletionServiceImpl.setCompletionPhase(phase);
656     phase.ignoreCurrentDocumentChange();
657
658     final Project project = getProject();
659     ApplicationManager.getApplication().invokeLater(new Runnable() {
660       @Override
661       public void run() {
662         CompletionAutoPopupHandler.runLaterWithCommitted(project, myEditor.getDocument(), new Runnable() {
663           @Override
664           public void run() {
665             if (phase.checkExpired()) return;
666
667             CompletionAutoPopupHandler.invokeCompletion(myParameters.getCompletionType(),
668                                                         isAutopopupCompletion(), project, myEditor, myParameters.getInvocationCount(),
669                                                         true);
670           }
671         });
672       }
673     }, project.getDisposed());
674   }
675
676   @Override
677   public String toString() {
678     return "CompletionProgressIndicator[count=" +
679            myCount +
680            ",phase=" +
681            CompletionServiceImpl.getCompletionPhase() +
682            "]@" +
683            System.identityHashCode(this);
684   }
685
686   protected void handleEmptyLookup(final boolean awaitSecondInvocation) {
687     if (isAutopopupCompletion() && ApplicationManager.getApplication().isUnitTestMode()) {
688       return;
689     }
690
691     LOG.assertTrue(!isAutopopupCompletion());
692
693     if (ApplicationManager.getApplication().isUnitTestMode() || !myHandler.invokedExplicitly) {
694       CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion);
695       return;
696     }
697
698     for (final CompletionContributor contributor : CompletionContributor.forParameters(getParameters())) {
699       final String text = contributor.handleEmptyLookup(getParameters(), getEditor());
700       if (StringUtil.isNotEmpty(text)) {
701         LightweightHint hint = showErrorHint(getProject(), getEditor(), text);
702         CompletionServiceImpl.setCompletionPhase(
703           awaitSecondInvocation ? new CompletionPhase.NoSuggestionsHint(hint, this) : CompletionPhase.NoCompletion);
704         return;
705       }
706     }
707     CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion);
708   }
709
710   private static LightweightHint showErrorHint(Project project, Editor editor, String text) {
711     final LightweightHint[] result = {null};
712     final EditorHintListener listener = new EditorHintListener() {
713       @Override
714       public void hintShown(final Project project, final LightweightHint hint, final int flags) {
715         result[0] = hint;
716       }
717     };
718     final MessageBusConnection connection = project.getMessageBus().connect();
719     connection.subscribe(EditorHintListener.TOPIC, listener);
720     assert text != null;
721     HintManager.getInstance().showErrorHint(editor, text, HintManager.UNDER);
722     connection.disconnect();
723     return result[0];
724   }
725
726   private static boolean shouldPreselectFirstSuggestion(CompletionParameters parameters) {
727     if (!Registry.is("ide.completion.autopopup.choose.by.enter")) {
728       return false;
729     }
730
731     if (Registry.is("ide.completion.lookup.element.preselect.depends.on.context")) {
732       for (CompletionPreselectionBehaviourProvider provider : Extensions.getExtensions(CompletionPreselectionBehaviourProvider.EP_NAME)) {
733         if (!provider.shouldPreselectFirstSuggestion(parameters)) {
734           return false;
735         }
736       }
737     }
738
739     if (!ApplicationManager.getApplication().isUnitTestMode()) {
740       return true;
741     }
742
743     switch (CodeInsightSettings.getInstance().AUTOPOPUP_FOCUS_POLICY) {
744       case CodeInsightSettings.ALWAYS:
745         return true;
746       case CodeInsightSettings.NEVER:
747         return false;
748     }
749
750     final Language language = PsiUtilCore.getLanguageAtOffset(parameters.getPosition().getContainingFile(), parameters.getOffset());
751     for (CompletionConfidence confidence : CompletionConfidenceEP.forLanguage(language)) {
752       //noinspection deprecation
753       final ThreeState result = confidence.shouldFocusLookup(parameters);
754       if (result != ThreeState.UNSURE) {
755         LOG.debug(confidence + " has returned shouldFocusLookup=" + result);
756         return result == ThreeState.YES;
757       }
758     }
759     return false;
760   }
761
762   void startCompletion(final CompletionInitializationContext initContext) {
763     boolean sync = ApplicationManager.getApplication().isUnitTestMode() && !CompletionAutoPopupHandler.ourTestingAutopopup;
764     final CompletionThreading strategy = sync ? new SyncCompletion() : new AsyncCompletion();
765
766     strategy.startThread(ProgressWrapper.wrap(this), new Runnable() {
767       @Override
768       public void run() {
769         scheduleAdvertising();
770       }
771     });
772     final WeighingDelegate weigher = strategy.delegateWeighing(this);
773
774     class CalculateItems implements Runnable {
775       @Override
776       public void run() {
777         try {
778           calculateItems(initContext, weigher);
779         }
780         catch (ProcessCanceledException ignore) {
781           cancel(); // some contributor may just throw PCE; if indicator is not canceled everything will hang
782         }
783         catch (Throwable t) {
784           cancel();
785           LOG.error(t);
786         }
787       }
788     }
789     strategy.startThread(this, new CalculateItems());
790   }
791
792   private LookupElement[] calculateItems(CompletionInitializationContext initContext, WeighingDelegate weigher) {
793     duringCompletion(initContext);
794     ProgressManager.checkCanceled();
795
796     LookupElement[] result = CompletionService.getCompletionService().performCompletion(myParameters, weigher);
797     ProgressManager.checkCanceled();
798
799     weigher.waitFor();
800     ProgressManager.checkCanceled();
801
802     return result;
803   }
804
805   public void addAdvertisement(@NotNull final String text, @Nullable final Color bgColor) {
806     myAdvertiserChanges.offer(new Runnable() {
807       @Override
808       public void run() {
809         myLookup.addAdvertisement(text, bgColor);
810       }
811     });
812
813     myQueue.queue(myUpdate);
814   }
815
816   private static class ModifierTracker extends KeyAdapter {
817     private final JComponent myContentComponent;
818
819     public ModifierTracker(JComponent contentComponent) {
820       myContentComponent = contentComponent;
821     }
822
823     @Override
824     public void keyPressed(KeyEvent e) {
825       processModifier(e);
826     }
827
828     @Override
829     public void keyReleased(KeyEvent e) {
830       processModifier(e);
831     }
832
833     private void processModifier(KeyEvent e) {
834       final int code = e.getKeyCode();
835       if (code == KeyEvent.VK_CONTROL || code == KeyEvent.VK_META || code == KeyEvent.VK_ALT || code == KeyEvent.VK_SHIFT) {
836         myContentComponent.removeKeyListener(this);
837         final CompletionPhase phase = CompletionServiceImpl.getCompletionPhase();
838         if (phase instanceof CompletionPhase.BgCalculation) {
839           ((CompletionPhase.BgCalculation)phase).modifiersChanged = true;
840         }
841         else if (phase instanceof CompletionPhase.InsertedSingleItem) {
842           CompletionServiceImpl.setCompletionPhase(CompletionPhase.NoCompletion);
843         }
844       }
845     }
846   }
847 }