fix completion popup crop to dialog size for single line editors (see copy class...
[idea/community.git] / platform / lang-impl / src / com / intellij / codeInsight / completion / CompletionProgressIndicator.java
1 /*
2  * Copyright 2000-2009 JetBrains s.r.o.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.intellij.codeInsight.completion;
18
19 import com.intellij.codeInsight.completion.impl.CompletionServiceImpl;
20 import com.intellij.codeInsight.lookup.LookupAdapter;
21 import com.intellij.codeInsight.lookup.LookupElement;
22 import com.intellij.codeInsight.lookup.LookupEvent;
23 import com.intellij.codeInsight.lookup.LookupManager;
24 import com.intellij.codeInsight.lookup.impl.LookupImpl;
25 import com.intellij.openapi.application.Application;
26 import com.intellij.openapi.application.ApplicationManager;
27 import com.intellij.openapi.application.Result;
28 import com.intellij.openapi.command.CommandProcessor;
29 import com.intellij.openapi.command.WriteCommandAction;
30 import com.intellij.openapi.diagnostic.Logger;
31 import com.intellij.openapi.editor.CaretModel;
32 import com.intellij.openapi.editor.Document;
33 import com.intellij.openapi.editor.Editor;
34 import com.intellij.openapi.editor.SelectionModel;
35 import com.intellij.openapi.editor.event.*;
36 import com.intellij.openapi.progress.ProgressManager;
37 import com.intellij.openapi.progress.util.ProgressIndicatorBase;
38 import com.intellij.openapi.util.Computable;
39 import com.intellij.ui.HintListener;
40 import com.intellij.ui.LightweightHint;
41 import com.intellij.util.concurrency.Semaphore;
42 import com.intellij.util.ui.AsyncProcessIcon;
43 import com.intellij.util.ui.update.MergingUpdateQueue;
44 import com.intellij.util.ui.update.Update;
45 import org.jetbrains.annotations.Nullable;
46 import org.jetbrains.annotations.TestOnly;
47
48 import javax.swing.*;
49 import java.awt.event.KeyAdapter;
50 import java.awt.event.KeyEvent;
51 import java.util.Collections;
52 import java.util.EventObject;
53 import java.util.List;
54
55 /**
56  * @author peter
57  */
58 public class CompletionProgressIndicator extends ProgressIndicatorBase implements CompletionProcess{
59   private static final Logger LOG = Logger.getInstance("#com.intellij.codeInsight.completion.CompletionProgressIndicator");
60   private final Editor myEditor;
61   private final CompletionParameters myParameters;
62   private final CodeCompletionHandlerBase myHandler;
63   private final LookupImpl myLookup;
64   private final MergingUpdateQueue myQueue;
65   private boolean myDisposed;
66   private boolean myInitialized;
67   private int myCount;
68   private final Update myUpdate = new Update("update") {
69     public void run() {
70       updateLookup();
71     }
72   };
73   private LightweightHint myHint;
74   private final CompletionContext myContextOriginal;
75   private final Semaphore myFreezeSemaphore;
76
77   private boolean myModifiersReleased;
78
79   private String myOldDocumentText;
80   private int myOldCaret;
81   private int myOldStart;
82   private int myOldEnd;
83
84   public CompletionProgressIndicator(final Editor editor, CompletionParameters parameters, CodeCompletionHandlerBase handler,
85                                      final CompletionContext contextOriginal, Semaphore freezeSemaphore) {
86     myEditor = editor;
87     myParameters = parameters;
88     myHandler = handler;
89     myContextOriginal = contextOriginal;
90     myFreezeSemaphore = freezeSemaphore;
91
92     myLookup = (LookupImpl)LookupManager.getInstance(editor.getProject()).createLookup(editor, LookupElement.EMPTY_ARRAY, "", new CompletionLookupArranger(parameters));
93     if (editor.isOneLineMode()) {
94       myLookup.setForceShowAsPopup(true);
95       myLookup.setCancelOnClickOutside(true);
96       myLookup.setCancelOnOtherWindowOpen(true);
97       myLookup.setResizable(false);
98     }
99
100     myLookup.addLookupListener(new LookupAdapter() {
101       public void itemSelected(LookupEvent event) {
102         cancel();
103         finishCompletion();
104
105         LookupElement item = event.getItem();
106         if (item == null) return;
107
108         setMergeCommand();
109
110         contextOriginal.setStartOffset(myEditor.getCaretModel().getOffset() - item.getLookupString().length());
111         CodeCompletionHandlerBase.selectLookupItem(item, event.getCompletionChar(), contextOriginal, myLookup.getItems());
112       }
113
114
115       public void lookupCanceled(final LookupEvent event) {
116         cancel();
117         finishCompletion();
118       }
119     });
120     myLookup.setCalculating(true);
121
122     myQueue = new MergingUpdateQueue("completion lookup progress", 200, true, myEditor.getContentComponent());
123
124     ApplicationManager.getApplication().assertIsDispatchThread();
125     registerItself();
126
127     scheduleAdvertising();
128
129     trackModifiers();
130   }
131
132   private void scheduleAdvertising() {
133     ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
134       public void run() {
135         if (isOutdated()) return; //tests?
136         final List<CompletionContributor> list = ApplicationManager.getApplication().runReadAction(new Computable<List<CompletionContributor>>() {
137           public List<CompletionContributor> compute() {
138             if (isOutdated()) {
139               return Collections.emptyList();
140             }
141
142             return CompletionContributor.forParameters(myParameters);
143           }
144         });
145         for (final CompletionContributor contributor : list) {
146           if (myLookup.getAdvertisementText() != null) return;
147           if (!myLookup.isCalculating() && !myLookup.isVisible()) return;
148
149           String s = ApplicationManager.getApplication().runReadAction(new Computable<String>() {
150             @Nullable
151             public String compute() {
152               if (isOutdated()) {
153                 return null;
154               }
155
156               return contributor.advertise(myParameters);
157             }
158           });
159           if (myLookup.getAdvertisementText() != null) return;
160
161           if (s != null) {
162             myLookup.setAdvertisementText(s);
163             ApplicationManager.getApplication().invokeLater(new Runnable() {
164               public void run() {
165                 if (isOutdated() || myEditor.getComponent().getRootPane() == null) {
166                   return;
167                 }
168                 updateLookup();
169               }
170             }, myQueue.getModalityState());
171             return;
172           }
173         }
174       }
175     });
176   }
177
178   private boolean isOutdated() {
179     return myEditor.isDisposed() || myDisposed;
180   }
181
182   private void trackModifiers() {
183     final JComponent contentComponent = myEditor.getContentComponent();
184     contentComponent.addKeyListener(new KeyAdapter() {
185       public void keyPressed(KeyEvent e) {
186         processModifier(e);
187       }
188
189       public void keyReleased(KeyEvent e) {
190         processModifier(e);
191       }
192
193       private void processModifier(KeyEvent e) {
194         final int code = e.getKeyCode();
195         if (code == KeyEvent.VK_CONTROL || code == KeyEvent.VK_META || code == KeyEvent.VK_ALT || code == KeyEvent.VK_SHIFT) {
196           myModifiersReleased = true;
197           if (myOldDocumentText != null) {
198             cleanup();
199           }
200           contentComponent.removeKeyListener(this);
201         }
202       }
203     });
204   }
205
206   private void setMergeCommand() {
207     CommandProcessor.getInstance().setCurrentCommandGroupId("Completion" + hashCode());
208   }
209
210   public void showLookup() {
211     updateLookup();
212   }
213
214   public CompletionParameters getParameters() {
215     return myParameters;
216   }
217
218   private void registerItself() {
219     CompletionServiceImpl.getCompletionService().setCurrentCompletion(this);
220   }
221
222   public void liveAfterDeath(@Nullable final LightweightHint hint) {
223     if (myModifiersReleased || ApplicationManager.getApplication().isUnitTestMode()) {
224       return;
225     }
226
227     registerItself();
228     myHint = hint;
229     if (hint != null) {
230       hint.addHintListener(new HintListener() {
231         public void hintHidden(final EventObject event) {
232           hint.removeHintListener(this);
233           cleanup();
234         }
235       });
236     }
237     final Document document = myEditor.getDocument();
238     document.addDocumentListener(new DocumentAdapter() {
239       @Override
240       public void beforeDocumentChange(DocumentEvent e) {
241         document.removeDocumentListener(this);
242         cleanup();
243       }
244     });
245     final SelectionModel selectionModel = myEditor.getSelectionModel();
246     selectionModel.addSelectionListener(new SelectionListener() {
247       public void selectionChanged(SelectionEvent e) {
248         selectionModel.removeSelectionListener(this);
249         cleanup();
250       }
251     });
252     final CaretModel caretModel = myEditor.getCaretModel();
253     caretModel.addCaretListener(new CaretListener() {
254       public void caretPositionChanged(CaretEvent e) {
255         caretModel.removeCaretListener(this);
256         cleanup();
257       }
258     });
259   }
260
261   public CodeCompletionHandlerBase getHandler() {
262     return myHandler;
263   }
264
265   public LookupImpl getLookup() {
266     return myLookup;
267   }
268
269   private void updateLookup() {
270     ApplicationManager.getApplication().assertIsDispatchThread();
271     if (isOutdated()) return;
272
273     if (!myInitialized) {
274       myInitialized = true;
275       if (myLookup.isCalculating()) {
276         final AsyncProcessIcon processIcon = myLookup.getProcessIcon();
277         processIcon.setVisible(true);
278         processIcon.resume();
279       }
280       myLookup.show();
281     }
282     myLookup.refreshUi();
283   }
284
285   public int getCount() {
286     return myCount;
287   }
288
289   private boolean isInsideIdentifier() {
290     return myContextOriginal.getOffsetMap().getOffset(CompletionInitializationContext.IDENTIFIER_END_OFFSET) != myContextOriginal.getSelectionEndOffset();
291   }
292
293
294   public synchronized void addItem(final LookupElement item) {
295     if (!isRunning()) return;
296     ProgressManager.checkCanceled();
297
298     final boolean unitTestMode = ApplicationManager.getApplication().isUnitTestMode();
299     if (!unitTestMode) {
300       assert !ApplicationManager.getApplication().isDispatchThread();
301     }
302
303     myLookup.addItem(item);
304     myCount++;
305     if (unitTestMode) return;
306
307     if (myCount == 1) {
308       ApplicationManager.getApplication().executeOnPooledThread(new Runnable() {
309         public void run() {
310           try {
311             Thread.sleep(300);
312           }
313           catch (InterruptedException e) {
314             LOG.error(e);
315           }
316           myFreezeSemaphore.up();
317         }
318       });
319     }
320     myQueue.queue(myUpdate);
321   }
322
323   public void closeAndFinish() {
324     if (myHint != null) {
325       myHint.hide();
326     }
327     LookupManager.getInstance(myEditor.getProject()).hideActiveLookup();
328   }
329
330   private void finishCompletion() {
331     assert !myDisposed;
332     myDisposed = true;
333
334     ApplicationManager.getApplication().assertIsDispatchThread();
335     myQueue.dispose();
336     cleanup();
337   }
338
339   @TestOnly
340   public static void cleanupForNextTest() {
341     CompletionProgressIndicator currentCompletion = CompletionServiceImpl.getCompletionService().getCurrentCompletion();
342     if (currentCompletion != null) {
343       currentCompletion.finishCompletion();
344     }
345   }
346
347   private void cleanup() {
348     myHint = null;
349     myOldDocumentText = null;
350     CompletionServiceImpl.getCompletionService().setCurrentCompletion(null);
351   }
352
353   public void stop() {
354     myQueue.cancelAllUpdates();
355     super.stop();
356
357     invokeLaterIfNotDispatch(new Runnable() {
358       public void run() {
359         if (isCanceled()) return;
360
361         if (myLookup.isVisible()) {
362           myLookup.getProcessIcon().suspend();
363           myLookup.getProcessIcon().setVisible(false);
364           updateLookup();
365         }
366       }
367     });
368
369   }
370
371   private void invokeLaterIfNotDispatch(final Runnable runnable) {
372     final Application application = ApplicationManager.getApplication();
373     if (application.isDispatchThread() || application.isUnitTestMode()) {
374       runnable.run();
375     } else {
376       application.invokeLater(runnable, myQueue.getModalityState());
377     }
378   }
379
380   public boolean fillInCommonPrefix(final boolean explicit) {
381     if (isInsideIdentifier()) {
382       return false;
383     }
384
385     final Boolean aBoolean = new WriteCommandAction<Boolean>(myEditor.getProject()) {
386       protected void run(Result<Boolean> result) throws Throwable {
387         if (!explicit) {
388           setMergeCommand();
389         }
390         try {
391           result.setResult(myLookup.fillInCommonPrefix(explicit));
392         }
393         catch (Exception e) {
394           LOG.error(e);
395         }
396       }
397     }.execute().getResultObject();
398     return aBoolean.booleanValue();
399   }
400
401   public boolean isInitialized() {
402     return myInitialized;
403   }
404
405   public void restorePrefix() {
406     setMergeCommand();
407
408     if (myOldDocumentText != null) {
409       myEditor.getDocument().setText(myOldDocumentText);
410       myEditor.getSelectionModel().setSelection(myOldStart, myOldEnd);
411       myEditor.getCaretModel().moveToOffset(myOldCaret);
412       myOldDocumentText = null;
413       return;
414     }
415
416     getLookup().restorePrefix();
417   }
418
419   public Editor getEditor() {
420     return myEditor;
421   }
422
423   public void rememberDocumentState() {
424     if (myModifiersReleased) {
425       return;
426     }
427
428     myOldDocumentText = myEditor.getDocument().getText();
429     myOldCaret = myEditor.getCaretModel().getOffset();
430     myOldStart = myEditor.getSelectionModel().getSelectionStart();
431     myOldEnd = myEditor.getSelectionModel().getSelectionEnd();
432   }
433
434 }