a5570e0450270ace339aa5171b83777925925137
[idea/community.git] / platform / lang-impl / src / com / intellij / execution / impl / ConsoleViewImpl.java
1 /*
2  * Copyright 2000-2016 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.execution.impl;
18
19 import com.google.common.base.CharMatcher;
20 import com.intellij.codeInsight.navigation.IncrementalSearchHandler;
21 import com.intellij.codeInsight.template.impl.editorActions.TypedActionHandlerBase;
22 import com.intellij.execution.ConsoleFolding;
23 import com.intellij.execution.ExecutionBundle;
24 import com.intellij.execution.actions.ConsoleActionsPostProcessor;
25 import com.intellij.execution.actions.EOFAction;
26 import com.intellij.execution.filters.*;
27 import com.intellij.execution.process.ProcessHandler;
28 import com.intellij.execution.ui.ConsoleView;
29 import com.intellij.execution.ui.ConsoleViewContentType;
30 import com.intellij.execution.ui.ObservableConsoleView;
31 import com.intellij.icons.AllIcons;
32 import com.intellij.ide.CommonActionsManager;
33 import com.intellij.ide.OccurenceNavigator;
34 import com.intellij.openapi.Disposable;
35 import com.intellij.openapi.actionSystem.*;
36 import com.intellij.openapi.application.ApplicationManager;
37 import com.intellij.openapi.application.ModalityState;
38 import com.intellij.openapi.command.CommandProcessor;
39 import com.intellij.openapi.command.undo.UndoUtil;
40 import com.intellij.openapi.diagnostic.Logger;
41 import com.intellij.openapi.editor.*;
42 import com.intellij.openapi.editor.actionSystem.*;
43 import com.intellij.openapi.editor.actions.ScrollToTheEndToolbarAction;
44 import com.intellij.openapi.editor.actions.ToggleUseSoftWrapsToolbarAction;
45 import com.intellij.openapi.editor.colors.EditorColorsScheme;
46 import com.intellij.openapi.editor.event.*;
47 import com.intellij.openapi.editor.ex.DocumentEx;
48 import com.intellij.openapi.editor.ex.EditorEx;
49 import com.intellij.openapi.editor.ex.util.EditorUtil;
50 import com.intellij.openapi.editor.highlighter.EditorHighlighter;
51 import com.intellij.openapi.editor.highlighter.HighlighterClient;
52 import com.intellij.openapi.editor.highlighter.HighlighterIterator;
53 import com.intellij.openapi.editor.impl.DocumentImpl;
54 import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces;
55 import com.intellij.openapi.editor.markup.RangeHighlighter;
56 import com.intellij.openapi.editor.markup.TextAttributes;
57 import com.intellij.openapi.extensions.Extensions;
58 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
59 import com.intellij.openapi.ide.CopyPasteManager;
60 import com.intellij.openapi.keymap.Keymap;
61 import com.intellij.openapi.keymap.KeymapManager;
62 import com.intellij.openapi.project.DumbAware;
63 import com.intellij.openapi.project.DumbAwareAction;
64 import com.intellij.openapi.project.DumbService;
65 import com.intellij.openapi.project.Project;
66 import com.intellij.openapi.util.*;
67 import com.intellij.openapi.util.registry.Registry;
68 import com.intellij.openapi.util.text.StringUtil;
69 import com.intellij.psi.search.GlobalSearchScope;
70 import com.intellij.psi.tree.IElementType;
71 import com.intellij.ui.EditorNotificationPanel;
72 import com.intellij.ui.awt.RelativePoint;
73 import com.intellij.util.*;
74 import com.intellij.util.text.CharArrayUtil;
75 import com.intellij.util.ui.UIUtil;
76 import gnu.trove.TIntObjectHashMap;
77 import org.jetbrains.annotations.NonNls;
78 import org.jetbrains.annotations.NotNull;
79 import org.jetbrains.annotations.Nullable;
80
81 import javax.swing.*;
82 import java.awt.*;
83 import java.awt.datatransfer.DataFlavor;
84 import java.awt.event.MouseAdapter;
85 import java.awt.event.MouseEvent;
86 import java.awt.event.MouseWheelEvent;
87 import java.io.IOException;
88 import java.util.*;
89 import java.util.List;
90 import java.util.concurrent.CopyOnWriteArraySet;
91
92 public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableConsoleView, DataProvider, OccurenceNavigator {
93   @NonNls private static final String CONSOLE_VIEW_POPUP_MENU = "ConsoleView.PopupMenu";
94   private static final Logger LOG = Logger.getInstance("#com.intellij.execution.impl.ConsoleViewImpl");
95
96   private static final int DEFAULT_FLUSH_DELAY = SystemProperties.getIntProperty("console.flush.delay.ms", 200);
97
98   private static final CharMatcher NEW_LINE_MATCHER = CharMatcher.anyOf("\n\r");
99
100   public static final Key<ConsoleViewImpl> CONSOLE_VIEW_IN_EDITOR_VIEW = Key.create("CONSOLE_VIEW_IN_EDITOR_VIEW");
101
102   private static boolean ourTypedHandlerInitialized;
103
104   private static synchronized void initTypedHandler() {
105     if (ourTypedHandlerInitialized) return;
106     final EditorActionManager actionManager = EditorActionManager.getInstance();
107     final TypedAction typedAction = actionManager.getTypedAction();
108     typedAction.setupHandler(new MyTypedHandler(typedAction.getHandler()));
109     ourTypedHandlerInitialized = true;
110   }
111
112
113   private final CommandLineFolding myCommandLineFolding = new CommandLineFolding();
114
115   private final DisposedPsiManagerCheck myPsiDisposedCheck;
116   private final boolean myIsViewer;
117
118   private ConsoleState myState;
119
120   private final Alarm mySpareTimeAlarm = new Alarm(this);
121   @Nullable
122   private final Alarm myHeavyAlarm;
123   private volatile int myHeavyUpdateTicket;
124
125   private final Collection<ChangeListener> myListeners = new CopyOnWriteArraySet<ChangeListener>();
126   private final List<AnAction> customActions = new ArrayList<AnAction>();
127   private final ConsoleBuffer myBuffer = new ConsoleBuffer();
128   private boolean myUpdateFoldingsEnabled = true;
129   private EditorHyperlinkSupport myHyperlinks;
130   private MyDiffContainer myJLayeredPane;
131   private JPanel myMainPanel;
132   private boolean myAllowHeavyFilters;
133   private boolean myLastStickingToEnd;
134   private boolean myCancelStickToEnd;
135
136   private boolean myTooMuchOfOutput;
137   private boolean myInDocumentUpdate;
138
139   // If true, then a document is being cleared right now.
140   // Should be accessed in EDT only.
141   @SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
142   private boolean myDocumentClearing;
143   private int myLastAddedTextLength;
144   private int consoleTooMuchTextBufferRatio;
145
146   public Editor getEditor() {
147     return myEditor;
148   }
149
150   public EditorHyperlinkSupport getHyperlinks() {
151     return myHyperlinks;
152   }
153
154   public void scrollToEnd() {
155     if (myEditor == null) return;
156     EditorUtil.scrollToTheEnd(myEditor);
157     myCancelStickToEnd = false;
158   }
159
160   public void foldImmediately() {
161     ApplicationManager.getApplication().assertIsDispatchThread();
162     if (!myFlushAlarm.isEmpty()) {
163       cancelAllFlushRequests();
164       new MyFlushRunnable().run();
165     }
166
167     myFoldingAlarm.cancelAllRequests();
168
169     myPendingFoldRegions.clear();
170     final FoldingModel model = myEditor.getFoldingModel();
171     model.runBatchFoldingOperation(new Runnable() {
172       @Override
173       public void run() {
174         for (FoldRegion region : model.getAllFoldRegions()) {
175           model.removeFoldRegion(region);
176         }
177       }
178     });
179     myFolding.clear();
180
181     updateFoldings(0, myEditor.getDocument().getLineCount() - 1, true);
182   }
183
184   static class TokenInfo {
185     final ConsoleViewContentType contentType;
186     int startOffset;
187     int endOffset;
188
189     TokenInfo(ConsoleViewContentType contentType, int startOffset, int endOffset) {
190       this.contentType = contentType;
191       this.startOffset = startOffset;
192       this.endOffset = endOffset;
193     }
194
195     public int getLength() {
196       return endOffset - startOffset;
197     }
198
199     @Override
200     public String toString() {
201       return contentType + "[" + startOffset + ";" + endOffset + "]";
202     }
203
204     @Nullable
205     public HyperlinkInfo getHyperlinkInfo() {
206       return null;
207     }
208   }
209
210   static class HyperlinkTokenInfo extends TokenInfo {
211     private final HyperlinkInfo myHyperlinkInfo;
212
213     HyperlinkTokenInfo(final ConsoleViewContentType contentType, final int startOffset, final int endOffset, HyperlinkInfo hyperlinkInfo) {
214       super(contentType, startOffset, endOffset);
215       myHyperlinkInfo = hyperlinkInfo;
216     }
217
218     @Override
219     public HyperlinkInfo getHyperlinkInfo() {
220       return myHyperlinkInfo;
221     }
222   }
223
224   private final Project myProject;
225
226   private boolean myOutputPaused;
227
228   private EditorEx myEditor;
229
230   private final Object LOCK = new Object();
231
232   /**
233    * Holds number of symbols managed by the current console.
234    * <p/>
235    * Total number is assembled as a sum of symbols that are already pushed to the document and number of deferred symbols that
236    * are awaiting to be pushed to the document.
237    */
238   private int myContentSize;
239
240   /**
241    * Holds information about lexical division by offsets of the text already pushed to document.
242    * <p/>
243    * Target offsets are anchored to the document here.
244    */
245   private final List<TokenInfo> myTokens = new ArrayList<TokenInfo>();
246
247   private final TIntObjectHashMap<ConsoleFolding> myFolding = new TIntObjectHashMap<ConsoleFolding>();
248
249   private String myHelpId;
250
251   private final Alarm myFlushUserInputAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
252   private final Alarm myFlushAlarm = new Alarm(this);
253
254   private final Set<MyFlushRunnable> myCurrentRequests = new HashSet<MyFlushRunnable>();
255
256   protected final CompositeFilter myFilters;
257
258   @Nullable private final InputFilter myInputMessageFilter;
259
260   private final Alarm myFoldingAlarm = new Alarm(this);
261   private final List<FoldRegion> myPendingFoldRegions = new ArrayList<FoldRegion>();
262
263   public ConsoleViewImpl(final Project project, boolean viewer) {
264     this(project, GlobalSearchScope.allScope(project), viewer, true);
265   }
266
267   public ConsoleViewImpl(@NotNull final Project project,
268                          @NotNull GlobalSearchScope searchScope,
269                          boolean viewer,
270                          boolean usePredefinedMessageFilter) {
271     this(project, searchScope, viewer,
272          new ConsoleState.NotStartedStated() {
273            @Override
274            public ConsoleState attachTo(ConsoleViewImpl console, ProcessHandler processHandler) {
275              return new ConsoleViewRunningState(console, processHandler, this, true, true);
276            }
277          },
278          usePredefinedMessageFilter);
279   }
280
281   protected ConsoleViewImpl(@NotNull final Project project,
282                             @NotNull GlobalSearchScope searchScope,
283                             boolean viewer,
284                             @NotNull final ConsoleState initialState,
285                             boolean usePredefinedMessageFilter)
286   {
287     super(new BorderLayout());
288     initTypedHandler();
289     myIsViewer = viewer;
290     myState = initialState;
291     myPsiDisposedCheck = new DisposedPsiManagerCheck(project);
292     myProject = project;
293
294     myFilters = new CompositeFilter(project);
295     if (usePredefinedMessageFilter) {
296       for (ConsoleFilterProvider eachProvider : Extensions.getExtensions(ConsoleFilterProvider.FILTER_PROVIDERS)) {
297         Filter[] filters;
298         if (eachProvider instanceof ConsoleDependentFilterProvider) {
299           filters = ((ConsoleDependentFilterProvider)eachProvider).getDefaultFilters(this, project, searchScope);
300         }
301         else if (eachProvider instanceof ConsoleFilterProviderEx) {
302           filters = ((ConsoleFilterProviderEx)eachProvider).getDefaultFilters(project, searchScope);
303         }
304         else {
305           filters = eachProvider.getDefaultFilters(project);
306         }
307         for (Filter filter : filters) {
308           myFilters.addFilter(filter);
309         }
310       }
311     }
312     myFilters.setForceUseAllFilters(true);
313     myHeavyUpdateTicket = 0;
314     myHeavyAlarm = myFilters.isAnyHeavy() ? new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this) : null;
315
316
317     ConsoleInputFilterProvider[] inputFilters = Extensions.getExtensions(ConsoleInputFilterProvider.INPUT_FILTER_PROVIDERS);
318     if (inputFilters.length > 0) {
319       CompositeInputFilter compositeInputFilter = new CompositeInputFilter(project);
320       myInputMessageFilter = compositeInputFilter;
321       for (ConsoleInputFilterProvider eachProvider : inputFilters) {
322         InputFilter[] filters = eachProvider.getDefaultFilters(project);
323         for (InputFilter filter : filters) {
324           compositeInputFilter.addFilter(filter);
325         }
326       }
327     }
328     else {
329       myInputMessageFilter = null;
330     }
331
332     consoleTooMuchTextBufferRatio = Registry.intValue("console.too.much.text.buffer.ratio");
333
334     project.getMessageBus().connect(this).subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
335       private long myLastStamp;
336
337       @Override
338       public void enteredDumbMode() {
339         if (myEditor == null) return;
340         myLastStamp = myEditor.getDocument().getModificationStamp();
341
342       }
343
344       @Override
345       public void exitDumbMode() {
346         ApplicationManager.getApplication().invokeLater(new Runnable() {
347           @Override
348           public void run() {
349             if (myEditor == null || project.isDisposed() || DumbService.getInstance(project).isDumb()) return;
350             
351             DocumentEx document = myEditor.getDocument();
352             if (myLastStamp != document.getModificationStamp()) {
353               clearHyperlinkAndFoldings();
354               highlightHyperlinksAndFoldings(document.createRangeMarker(0, 0));
355             }
356           }
357         });
358       }
359     });
360
361   }
362
363   @Override
364   public void attachToProcess(final ProcessHandler processHandler) {
365     myState = myState.attachTo(this, processHandler);
366   }
367
368   @Override
369   public void clear() {
370     if (myEditor == null) return;
371     synchronized (LOCK) {
372       // real document content will be cleared on next flush;
373       myContentSize = 0;
374       myBuffer.clear();
375       myFolding.clear();
376     }
377     if (myFlushAlarm.isDisposed()) return;
378     cancelAllFlushRequests();
379     addFlushRequest(new MyClearRunnable());
380     cancelHeavyAlarm();
381   }
382
383   @Override
384   public void scrollTo(final int offset) {
385     if (myEditor == null) return;
386     class ScrollRunnable extends MyFlushRunnable {
387       private final int myOffset = offset;
388
389       @Override
390       public void doRun() {
391         flushDeferredText();
392         if (myEditor == null) return;
393         int moveOffset = Math.min(offset, myEditor.getDocument().getTextLength());
394         if (myBuffer.isUseCyclicBuffer() && moveOffset >= myEditor.getDocument().getTextLength()) {
395           moveOffset = 0;
396         }
397         myEditor.getCaretModel().moveToOffset(moveOffset);
398         myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
399       }
400
401       @Override
402       public boolean equals(Object o) {
403         return super.equals(o) && myOffset == ((ScrollRunnable)o).myOffset;
404       }
405     }
406     addFlushRequest(new ScrollRunnable());
407   }
408
409   public void requestScrollingToEnd() {
410     if (myEditor == null) {
411       return;
412     }
413
414     addFlushRequest(new MyFlushRunnable() {
415       @Override
416       public void doRun() {
417         flushDeferredText();
418         if (myEditor == null || myFlushAlarm.isDisposed()) {
419           return;
420         }
421
422         scrollToEnd();
423       }
424     });
425   }
426
427   private void addFlushRequest(MyFlushRunnable scrollRunnable) {
428     addFlushRequest(scrollRunnable, 0);
429   }
430
431   private void addFlushRequest(@NotNull MyFlushRunnable flushRunnable, final int millis) {
432     synchronized (myCurrentRequests) {
433       if (!myFlushAlarm.isDisposed() && myCurrentRequests.add(flushRunnable)) {
434         myFlushAlarm.addRequest(flushRunnable, millis, getStateForUpdate());
435       }
436     }
437   }
438
439
440   private static void assertIsDispatchThread() {
441     ApplicationManager.getApplication().assertIsDispatchThread();
442   }
443
444   @Override
445   public void setOutputPaused(final boolean value) {
446     myOutputPaused = value;
447     if (!value) {
448       requestFlushImmediately();
449     }
450   }
451
452   @Override
453   public boolean isOutputPaused() {
454     return myOutputPaused;
455   }
456
457   public void setEmulateCarriageReturn(boolean emulate) {
458     myBuffer.setKeepSlashR(emulate);
459   }
460
461   @Override
462   public boolean hasDeferredOutput() {
463     synchronized (LOCK) {
464       return myBuffer.getLength() > 0;
465     }
466   }
467
468   @Override
469   public void performWhenNoDeferredOutput(final Runnable runnable) {
470     //Q: implement in another way without timer?
471     if (!hasDeferredOutput()) {
472       runnable.run();
473     }
474     else {
475       performLaterWhenNoDeferredOutput(runnable);
476     }
477   }
478
479   private void performLaterWhenNoDeferredOutput(final Runnable runnable) {
480     if (mySpareTimeAlarm.isDisposed()) return;
481     mySpareTimeAlarm.addRequest(
482       new Runnable() {
483         @Override
484         public void run() {
485           performWhenNoDeferredOutput(runnable);
486         }
487       },
488       100,
489       ModalityState.stateForComponent(myJLayeredPane)
490     );
491   }
492
493   @Override
494   public JComponent getComponent() {
495     if (myMainPanel == null) {
496       myMainPanel = new JPanel(new BorderLayout());
497       myJLayeredPane = new MyDiffContainer(myMainPanel, myFilters.getUpdateMessage());
498       Disposer.register(this, myJLayeredPane);
499       add(myJLayeredPane, BorderLayout.CENTER);
500     }
501
502     if (myEditor == null) {
503       initConsoleEditor();
504       requestFlushImmediately();
505       myMainPanel.add(createCenterComponent(), BorderLayout.CENTER);
506     }
507     return this;
508   }
509
510   /**
511    * Adds transparent (actually, non-opaque) component over console.
512    * It will be as big as console. Use it to draw on console because it does not prevent user from console usage.
513    *
514    * @param component component to add
515    */
516   public final void addLayerToPane(@NotNull final JComponent component) {
517     getComponent(); // Make sure component exists
518     component.setOpaque(false);
519     component.setVisible(true);
520     myJLayeredPane.add(component, 0);
521   }
522
523   private void initConsoleEditor() {
524     myEditor = createConsoleEditor();
525     registerConsoleEditorActions();
526     myEditor.getScrollPane().setBorder(null);
527     MouseAdapter mouseListener = new MouseAdapter() {
528       @Override
529       public void mousePressed(MouseEvent e) {
530         updateStickToEndState(true);
531       }
532
533       @Override
534       public void mouseDragged(MouseEvent e) {
535         updateStickToEndState(false);
536       }
537       
538       @Override
539       public void mouseWheelMoved(MouseWheelEvent e) {
540         updateStickToEndState(false);
541       }
542     };
543     myEditor.getScrollPane().addMouseWheelListener(mouseListener);
544     myEditor.getScrollPane().getVerticalScrollBar().addMouseListener(mouseListener);
545     myEditor.getScrollPane().getVerticalScrollBar().addMouseMotionListener(mouseListener);
546     myHyperlinks = new EditorHyperlinkSupport(myEditor, myProject);
547     myEditor.getScrollingModel().addVisibleAreaListener(new VisibleAreaListener() {
548       @Override
549       public void visibleAreaChanged(VisibleAreaEvent e) {
550         // There is a possible case that the console text is populated while the console is not shown (e.g. we're debugging and
551         // 'Debugger' tab is active while 'Console' is not). It's also possible that newly added text contains long lines that
552         // are soft wrapped. We want to update viewport position then when the console becomes visible.
553         Rectangle oldR = e.getOldRectangle();
554
555         if (oldR != null && oldR.height <= 0 &&
556             e.getNewRectangle().height > 0 &&
557             isStickingToEnd()) {
558           scrollToEnd();
559         }
560       }
561     });
562   }
563
564   private void updateStickToEndState(boolean useImmediatePosition) {
565     if (myEditor == null) return;
566
567     JScrollBar scrollBar = myEditor.getScrollPane().getVerticalScrollBar();
568     int scrollBarPosition = useImmediatePosition ? scrollBar.getValue() : 
569                             myEditor.getScrollingModel().getVisibleAreaOnScrollingFinished().y;
570     boolean vscrollAtBottom = scrollBarPosition == scrollBar.getMaximum() - scrollBar.getVisibleAmount();
571     boolean stickingToEnd = isStickingToEnd();
572
573     if (!vscrollAtBottom && stickingToEnd) {
574       myCancelStickToEnd = true;
575     } else if (vscrollAtBottom && !stickingToEnd) {
576       scrollToEnd();
577     }
578   }
579
580   protected JComponent createCenterComponent() {
581     return myEditor.getComponent();
582   }
583
584   @Override
585   public void dispose() {
586     myState = myState.dispose();
587     if (myEditor != null) {
588       cancelAllFlushRequests();
589       mySpareTimeAlarm.cancelAllRequests();
590       disposeEditor();
591       synchronized (LOCK) {
592         myBuffer.clear();
593       }
594       myEditor = null;
595       myHyperlinks = null;
596     }
597   }
598
599   private void cancelAllFlushRequests() {
600     synchronized (myCurrentRequests) {
601       for (MyFlushRunnable request : myCurrentRequests) {
602         request.invalidate();
603       }
604       myCurrentRequests.clear();
605       myFlushAlarm.cancelAllRequests();
606     }
607   }
608
609   protected void disposeEditor() {
610     UIUtil.invokeAndWaitIfNeeded(new Runnable() {
611       @Override
612       public void run() {
613         if (!myEditor.isDisposed()) {
614           EditorFactory.getInstance().releaseEditor(myEditor);
615         }
616       }
617     });
618   }
619
620   @Override
621   public void print(@NotNull String s, @NotNull ConsoleViewContentType contentType) {
622     if (myInputMessageFilter == null) {
623       printHyperlink(s, contentType, null);
624       return;
625     }
626
627     List<Pair<String, ConsoleViewContentType>> result = myInputMessageFilter.applyFilter(s, contentType);
628     if (result == null) {
629       printHyperlink(s, contentType, null);
630     }
631     else {
632       for (Pair<String, ConsoleViewContentType> pair : result) {
633         if (pair.first != null) {
634           printHyperlink(pair.first, pair.second == null ? contentType : pair.second, null);
635         }
636       }
637     }
638   }
639
640   private void printHyperlink(@NotNull String s, @NotNull ConsoleViewContentType contentType, @Nullable HyperlinkInfo info) {
641     synchronized (LOCK) {
642       Pair<String, Integer> pair = myBuffer.print(s, contentType, info);
643       s = pair.first;
644       myContentSize += s.length() - pair.second;
645
646       if (contentType == ConsoleViewContentType.USER_INPUT && NEW_LINE_MATCHER.indexIn(s) >= 0) {
647         flushDeferredUserInput();
648       }
649       if (myEditor != null) {
650         final boolean shouldFlushNow = myBuffer.isUseCyclicBuffer() && myBuffer.getLength() >= myBuffer.getCyclicBufferSize();
651         addFlushRequest(new MyFlushRunnable(), shouldFlushNow ? 0 : DEFAULT_FLUSH_DELAY);
652       }
653     }
654   }
655
656   private void addToken(int length, @Nullable HyperlinkInfo info, ConsoleViewContentType contentType) {
657     ConsoleUtil.addToken(length, info, contentType, myTokens);
658   }
659
660   private static ModalityState getStateForUpdate() {
661     return null;//myStateForUpdate != null ? myStateForUpdate.compute() : ModalityState.stateForComponent(this);
662   }
663
664   private void requestFlushImmediately() {
665     if (myEditor != null) {
666       addFlushRequest(new MyFlushRunnable());
667     }
668   }
669
670   @Override
671   public int getContentSize() {
672     synchronized (LOCK) {
673       return myContentSize;
674     }
675   }
676
677   @Override
678   public boolean canPause() {
679     return true;
680   }
681
682   public void flushDeferredText() {
683     flushDeferredText(false);
684   }
685
686   private void flushDeferredText(boolean clear) {
687     ApplicationManager.getApplication().assertIsDispatchThread();
688     if (myProject.isDisposed()) {
689       return;
690     }
691     EditorEx editor = myEditor;
692     if (editor == null) {
693       //already disposed
694       return;
695     }
696     final boolean shouldStickToEnd = clear || !myCancelStickToEnd && isStickingToEnd();
697     myCancelStickToEnd = false; // Cancel only needs to last for one update. Next time, isStickingToEnd() will be false.
698     if (clear) {
699       final DocumentEx document = editor.getDocument();
700       synchronized (LOCK) {
701         myTokens.clear();
702         clearHyperlinkAndFoldings();
703       }
704       final int documentTextLength = document.getTextLength();
705       if (documentTextLength > 0) {
706         CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
707           @Override
708           public void run() {
709             document.setInBulkUpdate(true);
710             try {
711               myInDocumentUpdate = true;
712               myDocumentClearing = true;
713               document.deleteString(0, documentTextLength);
714             }
715             finally {
716               document.setInBulkUpdate(false);
717               myDocumentClearing = false;
718               myInDocumentUpdate = false;
719             }
720           }
721         }, null, DocCommandGroupId.noneGroupId(document));
722       }
723       return;
724     }
725
726
727     final String addedText;
728     final Collection<ConsoleViewContentType> contentTypes;
729     int deferredTokensSize;
730     synchronized (LOCK) {
731       if (myOutputPaused) return;
732       if (myBuffer.isEmpty()) return;
733
734       addedText = myBuffer.getText();
735
736       contentTypes = Collections.unmodifiableCollection(new HashSet<ConsoleViewContentType>(myBuffer.getDeferredTokenTypes()));
737       List<TokenInfo> deferredTokens = myBuffer.getDeferredTokens();
738       for (TokenInfo deferredToken : deferredTokens) {
739         addToken(deferredToken.getLength(), deferredToken.getHyperlinkInfo(), deferredToken.contentType);
740       }
741       deferredTokensSize = deferredTokens.size();
742       myBuffer.clear(false);
743       cancelHeavyAlarm();
744     }
745     final Document document = myEditor.getDocument();
746     final RangeMarker lastProcessedOutput = document.createRangeMarker(document.getTextLength(), document.getTextLength());
747
748     CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
749       @Override
750       public void run() {
751         if (!shouldStickToEnd) {
752           myEditor.getScrollingModel().accumulateViewportChanges();
753         }
754         try {
755           myInDocumentUpdate = true;
756           String[] strings = addedText.split("\\r", -1); // limit must be any negative number to avoid discarding of trailing empty strings
757           for (int i = 0; i < strings.length - 1; i++) {
758             document.insertString(document.getTextLength(), strings[i]);
759             int lastLine = document.getLineCount() - 1;
760             if (lastLine >= 0) {
761               ConsoleUtil.updateTokensOnTextRemoval(myTokens, document.getTextLength(), document.getTextLength() + 1);
762               document.deleteString(document.getLineStartOffset(lastLine), document.getTextLength());
763             }
764           }
765           if (strings.length > 0) {
766             document.insertString(document.getTextLength(), strings[strings.length - 1]);
767             myContentSize -= strings.length - 1;
768           }
769         }
770         finally {
771           myInDocumentUpdate = false;
772           if (!shouldStickToEnd) {
773             myEditor.getScrollingModel().flushViewportChanges();
774           }
775         }
776         if (!contentTypes.isEmpty()) {
777           for (ChangeListener each : myListeners) {
778             each.contentAdded(contentTypes);
779           }
780         }
781       }
782     }, null, DocCommandGroupId.noneGroupId(document));
783     synchronized (LOCK) {
784       for (int i = myTokens.size() - 1; i >= 0 && deferredTokensSize > 0; i--, deferredTokensSize--) {
785         TokenInfo token = myTokens.get(i);
786         final HyperlinkInfo info = token.getHyperlinkInfo();
787         if (info != null) {
788           myHyperlinks.createHyperlink(token.startOffset, token.endOffset, null, info);
789         }
790       }
791     }
792     myPsiDisposedCheck.performCheck();
793     myLastAddedTextLength = addedText.length();
794     if (!myTooMuchOfOutput) {
795       if (isTheAmountOfTextTooBig(myLastAddedTextLength)) { // disable hyperlinks and folding until new output arriving slows down again
796         myTooMuchOfOutput = true;
797         final EditorNotificationPanel comp =
798           new EditorNotificationPanel().text("Too much output to process").icon(AllIcons.General.ExclMark);
799         final Alarm tooMuchOutputAlarm = new Alarm();
800         //show the notification with a delay to avoid blinking when "too much output" ceases quickly
801         tooMuchOutputAlarm.addRequest(new Runnable() {
802           @Override
803           public void run() {
804             add(comp, BorderLayout.NORTH);
805           }
806         }, 300);
807         performWhenNoDeferredOutput(new Runnable() {
808           @Override
809           public void run() {
810             if (!isTheAmountOfTextTooBig(myLastAddedTextLength)) {
811               try {
812                 highlightHyperlinksAndFoldings(lastProcessedOutput);
813               }
814               finally {
815                 myTooMuchOfOutput = false;
816                 remove(comp);
817                 tooMuchOutputAlarm.cancelAllRequests();
818               }
819             }
820             else {
821               myLastAddedTextLength = 0;
822               performLaterWhenNoDeferredOutput(this);
823             }
824           }
825         });
826       }
827       else {
828         highlightHyperlinksAndFoldings(lastProcessedOutput);
829       }
830     }
831
832     if (shouldStickToEnd) {
833       scrollToEnd();
834     }
835   }
836
837   private boolean isStickingToEnd() {
838     if (myEditor == null) return myLastStickingToEnd;
839     Document document = myEditor.getDocument();
840     int caretOffset = myEditor.getCaretModel().getOffset();
841     myLastStickingToEnd = document.getLineNumber(caretOffset) >= document.getLineCount() - 1;
842     return myLastStickingToEnd;
843   }
844
845   private boolean isTheAmountOfTextTooBig(final int textLength) {
846     return myBuffer.isUseCyclicBuffer() && textLength > myBuffer.getCyclicBufferSize() / consoleTooMuchTextBufferRatio;
847   }
848
849   private void clearHyperlinkAndFoldings() {
850     myEditor.getMarkupModel().removeAllHighlighters();
851
852     myPendingFoldRegions.clear();
853     myFolding.clear();
854     myFoldingAlarm.cancelAllRequests();
855     myEditor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
856       @Override
857       public void run() {
858         myEditor.getFoldingModel().clearFoldRegions();
859       }
860     });
861
862     cancelHeavyAlarm();
863   }
864
865   private void cancelHeavyAlarm() {
866     if (myHeavyAlarm != null && !myHeavyAlarm.isDisposed()) {
867       myHeavyAlarm.cancelAllRequests();
868       ++myHeavyUpdateTicket;
869     }
870   }
871
872   private void flushDeferredUserInput() {
873     final String textToSend = myBuffer.cutFirstUserInputLine();
874     if (textToSend == null) {
875       return;
876     }
877     myFlushUserInputAlarm.addRequest(new Runnable() {
878       @Override
879       public void run() {
880         if (myState.isRunning()) {
881           try {
882             // this may block forever, see IDEA-54340
883             myState.sendUserInput(textToSend);
884           }
885           catch (IOException ignored) {
886           }
887         }
888       }
889     }, 0);
890   }
891
892   @Override
893   public Object getData(final String dataId) {
894     if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
895       if (myEditor == null) {
896         return null;
897       }
898       final LogicalPosition pos = myEditor.getCaretModel().getLogicalPosition();
899       final HyperlinkInfo info = myHyperlinks.getHyperlinkInfoByLineAndCol(pos.line, pos.column);
900       final OpenFileDescriptor openFileDescriptor = info instanceof FileHyperlinkInfo ? ((FileHyperlinkInfo)info).getDescriptor() : null;
901       if (openFileDescriptor == null || !openFileDescriptor.getFile().isValid()) {
902         return null;
903       }
904       return openFileDescriptor;
905     }
906
907     if (CommonDataKeys.EDITOR.is(dataId)) {
908       return myEditor;
909     }
910     if (PlatformDataKeys.HELP_ID.is(dataId)) {
911       return myHelpId;
912     }
913     if (LangDataKeys.CONSOLE_VIEW.is(dataId)) {
914       return this;
915     }
916     return null;
917   }
918
919   @Override
920   public void setHelpId(final String helpId) {
921     myHelpId = helpId;
922   }
923
924   public void setUpdateFoldingsEnabled(boolean updateFoldingsEnabled) {
925     myUpdateFoldingsEnabled = updateFoldingsEnabled;
926   }
927
928   @Override
929   public void addMessageFilter(final Filter filter) {
930     myFilters.addFilter(filter);
931   }
932
933   @Override
934   public void printHyperlink(final String hyperlinkText, final HyperlinkInfo info) {
935     printHyperlink(hyperlinkText, ConsoleViewContentType.NORMAL_OUTPUT, info);
936   }
937
938   private EditorEx createConsoleEditor() {
939     return ApplicationManager.getApplication().runReadAction(new Computable<EditorEx>() {
940       @Override
941       public EditorEx compute() {
942         EditorEx editor = doCreateConsoleEditor();
943         LOG.assertTrue(UndoUtil.isUndoDisabledFor(editor.getDocument()));
944         editor.setContextMenuGroupId(null); // disabling default context menu
945         editor.addEditorMouseListener(new EditorPopupHandler() {
946           @Override
947           public void invokePopup(final EditorMouseEvent event) {
948             popupInvoked(event.getMouseEvent());
949           }
950         });
951         editor.getDocument().addDocumentListener(new DocumentAdapter() {
952           @Override
953           public void documentChanged(DocumentEvent event) {
954             onDocumentChanged(event);
955           }
956         }, ConsoleViewImpl.this);
957
958         int bufferSize = myBuffer.isUseCyclicBuffer() ? myBuffer.getCyclicBufferSize() : 0;
959         editor.getDocument().setCyclicBufferSize(bufferSize);
960
961         editor.putUserData(CONSOLE_VIEW_IN_EDITOR_VIEW, ConsoleViewImpl.this);
962
963         editor.getSettings().setAllowSingleLogicalLineFolding(true); // We want to fold long soft-wrapped command lines
964         editor.setHighlighter(createHighlighter());
965
966         return editor;
967       }
968     });
969   }
970
971   private void onDocumentChanged(DocumentEvent event) {
972     if (event.getNewLength() == 0) {
973       // string has been removed, adjust token ranges
974       synchronized (LOCK) {
975         ConsoleUtil.updateTokensOnTextRemoval(myTokens, event.getOffset(), event.getOffset() + event.getOldLength());
976         int toRemoveLen = event.getOldLength();
977         if (!myDocumentClearing) {
978           // If document is being cleared now, then this event has been occurred as a result of calling clear() method.
979           // At start clear() method sets 'myContentSize' to 0, so there is no need to perform update again.
980           // Moreover, performing update of 'myContentSize' breaks executing "console.print();" immediately after "console.clear();".
981           myContentSize -= Math.min(myContentSize, toRemoveLen);
982         }
983       }
984     }
985     else if (!myInDocumentUpdate) {
986       int newFragmentLength = event.getNewFragment().length();
987       // track external appends
988       if (event.getOldFragment().length() == 0 && newFragmentLength > 0) {
989         synchronized (LOCK) {
990           myContentSize += newFragmentLength;
991           addToken(newFragmentLength, null, ConsoleViewContentType.NORMAL_OUTPUT);
992         }
993       }
994       else {
995         LOG.warn("unhandled external change: " + event);
996       }
997     }
998   }
999
1000   protected EditorEx doCreateConsoleEditor() {
1001     return ConsoleViewUtil.setupConsoleEditor(myProject, true, false);
1002   }
1003
1004   protected MyHighlighter createHighlighter() {
1005     return new MyHighlighter();
1006   }
1007
1008   private void registerConsoleEditorActions() {
1009     Shortcut[] shortcuts = KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_GOTO_DECLARATION);
1010     CustomShortcutSet shortcutSet = new CustomShortcutSet(ArrayUtil.mergeArrays(shortcuts, CommonShortcuts.ENTER.getShortcuts()));
1011     new HyperlinkNavigationAction().registerCustomShortcutSet(shortcutSet, myEditor.getContentComponent());
1012
1013
1014     if (!myIsViewer) {
1015       new EnterHandler().registerCustomShortcutSet(CommonShortcuts.ENTER, myEditor.getContentComponent());
1016       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_PASTE, new PasteHandler());
1017       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_BACKSPACE, new BackSpaceHandler());
1018       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_DELETE, new DeleteHandler());
1019
1020       registerActionHandler(myEditor, EOFAction.ACTION_ID);
1021     }
1022   }
1023
1024   private static void registerActionHandler(final Editor editor, final String actionId) {
1025     AnAction action = ActionManager.getInstance().getAction(actionId);
1026     action.registerCustomShortcutSet(action.getShortcutSet(), editor.getContentComponent());
1027   }
1028
1029   private static void registerActionHandler(final Editor editor, final String actionId, final AnAction action) {
1030     final Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
1031     final Shortcut[] shortcuts = keymap.getShortcuts(actionId);
1032     action.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), editor.getContentComponent());
1033   }
1034
1035   private void popupInvoked(MouseEvent mouseEvent) {
1036     final ActionManager actionManager = ActionManager.getInstance();
1037     final HyperlinkInfo info = myHyperlinks != null ? myHyperlinks.getHyperlinkInfoByPoint(mouseEvent.getPoint()) : null;
1038     ActionGroup group = null;
1039     if (info instanceof HyperlinkWithPopupMenuInfo) {
1040       group = ((HyperlinkWithPopupMenuInfo)info).getPopupMenuGroup(mouseEvent);
1041     }
1042     if (group == null) {
1043       group = (ActionGroup)actionManager.getAction(CONSOLE_VIEW_POPUP_MENU);
1044     }
1045     final ConsoleActionsPostProcessor[] postProcessors = Extensions.getExtensions(ConsoleActionsPostProcessor.EP_NAME);
1046     AnAction[] result = group.getChildren(null);
1047
1048     for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
1049       result = postProcessor.postProcessPopupActions(this, result);
1050     }
1051     final DefaultActionGroup processedGroup = new DefaultActionGroup(result);
1052     final ActionPopupMenu menu = actionManager.createActionPopupMenu(ActionPlaces.EDITOR_POPUP, processedGroup);
1053     menu.getComponent().show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
1054   }
1055
1056   private void highlightHyperlinksAndFoldings(RangeMarker lastProcessedOutput) {
1057     boolean canHighlightHyperlinks = !myFilters.isEmpty();
1058
1059     if (!canHighlightHyperlinks && myUpdateFoldingsEnabled) {
1060       return;
1061     }
1062     final int line1 = lastProcessedOutput.isValid() ? myEditor.getDocument().getLineNumber(lastProcessedOutput.getEndOffset()) : 0;
1063     lastProcessedOutput.dispose();
1064     int endLine = myEditor.getDocument().getLineCount() - 1;
1065     ApplicationManager.getApplication().assertIsDispatchThread();
1066
1067     if (canHighlightHyperlinks) {
1068       myHyperlinks.highlightHyperlinks(myFilters, line1, endLine);
1069     }
1070
1071     if (myAllowHeavyFilters && myFilters.isAnyHeavy() && myFilters.shouldRunHeavy()) {
1072       runHeavyFilters(line1, endLine);
1073     }
1074     if (myUpdateFoldingsEnabled) {
1075       updateFoldings(line1, endLine, true);
1076     }
1077   }
1078
1079   private void runHeavyFilters(int line1, int endLine) {
1080     final int startLine = Math.max(0, line1);
1081
1082     final Document document = myEditor.getDocument();
1083     final int startOffset = document.getLineStartOffset(startLine);
1084     String text = document.getText(new TextRange(startOffset, document.getLineEndOffset(endLine)));
1085     final Document documentCopy = new DocumentImpl(text,true);
1086     documentCopy.setReadOnly(true);
1087
1088     myJLayeredPane.startUpdating();
1089     final int currentValue = myHeavyUpdateTicket;
1090     assert myHeavyAlarm != null;
1091     myHeavyAlarm.addRequest(new Runnable() {
1092       @Override
1093       public void run() {
1094         if (!myFilters.shouldRunHeavy()) return;
1095         try {
1096           myFilters.applyHeavyFilter(documentCopy, startOffset, startLine, new Consumer<FilterMixin.AdditionalHighlight>() {
1097             @Override
1098             public void consume(final FilterMixin.AdditionalHighlight additionalHighlight) {
1099               addFlushRequest(new MyFlushRunnable() {
1100                 @Override
1101                 public void doRun() {
1102                   if (myHeavyUpdateTicket != currentValue) return;
1103                   myHyperlinks.addHighlighter(additionalHighlight.getStart(), additionalHighlight.getEnd(),
1104                                               additionalHighlight.getTextAttributes(null));
1105                 }
1106
1107                 @Override
1108                 public boolean equals(Object o) {
1109                   return this == o && super.equals(o);
1110                 }
1111               });
1112             }
1113           });
1114         }
1115         finally {
1116           if (myHeavyAlarm.isEmpty()) {
1117             SwingUtilities.invokeLater(new Runnable() {
1118               @Override
1119               public void run() {
1120                 myJLayeredPane.finishUpdating();
1121               }
1122             });
1123           }
1124         }
1125       }
1126     }, 0);
1127   }
1128
1129   private void updateFoldings(final int line1, final int endLine, boolean immediately) {
1130     final Document document = myEditor.getDocument();
1131     final CharSequence chars = document.getCharsSequence();
1132     final int startLine = Math.max(0, line1);
1133     final List<FoldRegion> toAdd = new ArrayList<FoldRegion>();
1134     for (int line = startLine; line <= endLine; line++) {
1135       boolean flushOnly = line == endLine;
1136       /*
1137       Grep Console plugin allows to fold empty lines. We need to handle this case in a special way.
1138
1139       Multiple lines are grouped into one folding, but to know when you can create the folding,
1140       you need a line which does not belong to that folding.
1141       When a new line, or a chunk of lines is printed, #addFolding is called for that lines + for an empty string
1142       (which basically does only one thing, gets a folding displayed).
1143       We do not want to process that empty string, but also we do not want to wait for another line
1144       which will create and display the folding - we'd see an unfolded stacktrace until another text came and flushed it.
1145       So therefore the condition, the last line(empty string) should still flush, but not be processed by
1146       com.intellij.execution.ConsoleFolding.
1147        */
1148       addFolding(document, chars, line, toAdd, flushOnly);
1149     }
1150     if (!toAdd.isEmpty()) {
1151       doUpdateFolding(toAdd, immediately);
1152     }
1153   }
1154
1155   private void doUpdateFolding(final List<FoldRegion> toAdd, final boolean immediately) {
1156     assertIsDispatchThread();
1157     myPendingFoldRegions.addAll(toAdd);
1158
1159     myFoldingAlarm.cancelAllRequests();
1160     final Runnable runnable = new Runnable() {
1161       @Override
1162       public void run() {
1163         if (myEditor == null || myEditor.isDisposed()) {
1164           return;
1165         }
1166
1167         assertIsDispatchThread();
1168         final FoldingModel model = myEditor.getFoldingModel();
1169         final Runnable operation = new Runnable() {
1170           @Override
1171           public void run() {
1172             assertIsDispatchThread();
1173             for (FoldRegion region : myPendingFoldRegions) {
1174               region.setExpanded(false);
1175               model.addFoldRegion(region);
1176             }
1177             myPendingFoldRegions.clear();
1178           }
1179         };
1180         if (immediately) {
1181           model.runBatchFoldingOperation(operation);
1182         }
1183         else {
1184           model.runBatchFoldingOperationDoNotCollapseCaret(operation);
1185         }
1186       }
1187     };
1188     if (immediately || myPendingFoldRegions.size() > 100) {
1189       runnable.run();
1190     }
1191     else {
1192       myFoldingAlarm.addRequest(runnable, 50);
1193     }
1194   }
1195
1196   private void addFolding(Document document, CharSequence chars, int line, List<FoldRegion> toAdd, boolean flushOnly) {
1197     ConsoleFolding current = null;
1198     if (!flushOnly) {
1199       String commandLinePlaceholder = myCommandLineFolding.getPlaceholder(line);
1200       if (commandLinePlaceholder != null) {
1201         FoldRegion region = myEditor.getFoldingModel()
1202           .createFoldRegion(document.getLineStartOffset(line), document.getLineEndOffset(line), commandLinePlaceholder, null, false);
1203         toAdd.add(region);
1204         return;
1205       }
1206       current = foldingForLine(EditorHyperlinkSupport.getLineText(document, line, false));
1207       if (current != null) {
1208         myFolding.put(line, current);
1209       }
1210     }
1211
1212     final ConsoleFolding prevFolding = myFolding.get(line - 1);
1213     if (current == null && prevFolding != null) {
1214       final int lEnd = line - 1;
1215       int lStart = lEnd;
1216       while (prevFolding.equals(myFolding.get(lStart - 1))) lStart--;
1217
1218       for (int i = lStart; i <= lEnd; i++) {
1219         myFolding.remove(i);
1220       }
1221
1222       List<String> toFold = new ArrayList<String>(lEnd - lStart + 1);
1223       for (int i = lStart; i <= lEnd; i++) {
1224         toFold.add(EditorHyperlinkSupport.getLineText(document, i, false));
1225       }
1226
1227       int oStart = document.getLineStartOffset(lStart);
1228       if (oStart > 0) oStart--;
1229       int oEnd = CharArrayUtil.shiftBackward(chars, document.getLineEndOffset(lEnd) - 1, " \t") + 1;
1230
1231       String placeholder = prevFolding.getPlaceholderText(toFold);
1232       FoldRegion region = placeholder == null ? null : myEditor.getFoldingModel().createFoldRegion(oStart, oEnd, placeholder, null, false);
1233       if (region != null) {
1234         toAdd.add(region);
1235       }
1236     }
1237   }
1238
1239   @Nullable
1240   private static ConsoleFolding foldingForLine(String lineText) {
1241     for (ConsoleFolding folding : ConsoleFolding.EP_NAME.getExtensions()) {
1242       if (folding.shouldFoldLine(lineText)) {
1243         return folding;
1244       }
1245     }
1246     return null;
1247   }
1248
1249
1250   public static class ClearAllAction extends DumbAwareAction {
1251     private final ConsoleView myConsoleView;
1252
1253     @SuppressWarnings("unused")
1254     public ClearAllAction() {
1255       this(null);
1256     }
1257
1258     public ClearAllAction(ConsoleView consoleView) {
1259       super(ExecutionBundle.message("clear.all.from.console.action.name"), "Clear the contents of the console", AllIcons.Actions.GC);
1260       myConsoleView = consoleView;
1261     }
1262
1263     @Override
1264     public void update(AnActionEvent e) {
1265       boolean enabled = myConsoleView != null && myConsoleView.getContentSize() > 0;
1266       if (!enabled) {
1267         enabled = e.getData(LangDataKeys.CONSOLE_VIEW) != null;
1268         Editor editor = e.getData(CommonDataKeys.EDITOR);
1269         if (editor != null && editor.getDocument().getTextLength() == 0) {
1270           enabled = false;
1271         }
1272       }
1273       e.getPresentation().setEnabled(enabled);
1274     }
1275
1276     @Override
1277     public void actionPerformed(final AnActionEvent e) {
1278       final ConsoleView consoleView = myConsoleView != null ? myConsoleView : e.getData(LangDataKeys.CONSOLE_VIEW);
1279       if (consoleView != null) {
1280         consoleView.clear();
1281       }
1282     }
1283   }
1284
1285   private class MyHighlighter extends DocumentAdapter implements EditorHighlighter {
1286     private HighlighterClient myEditor;
1287
1288     @NotNull
1289     @Override
1290     public HighlighterIterator createIterator(final int startOffset) {
1291       final int startIndex = ConsoleUtil.findTokenInfoIndexByOffset(myTokens, startOffset);
1292
1293       return new HighlighterIterator() {
1294         private int myIndex = startIndex;
1295
1296         @Override
1297         public TextAttributes getTextAttributes() {
1298           return atEnd() ? null : getTokenInfo().contentType.getAttributes();
1299         }
1300
1301         @Override
1302         public int getStart() {
1303           return atEnd() ? 0 : getTokenInfo().startOffset;
1304         }
1305
1306         @Override
1307         public int getEnd() {
1308           return atEnd() ? 0 : getTokenInfo().endOffset;
1309         }
1310
1311         @Override
1312         public IElementType getTokenType() {
1313           return null;
1314         }
1315
1316         @Override
1317         public void advance() {
1318           myIndex++;
1319         }
1320
1321         @Override
1322         public void retreat() {
1323           myIndex--;
1324         }
1325
1326         @Override
1327         public boolean atEnd() {
1328           return myIndex < 0 || myIndex >= myTokens.size();
1329         }
1330
1331         @Override
1332         public Document getDocument() {
1333           return myEditor.getDocument();
1334         }
1335
1336         private TokenInfo getTokenInfo() {
1337           return myTokens.get(myIndex);
1338         }
1339       };
1340     }
1341
1342     @Override
1343     public void setText(@NotNull final CharSequence text) {
1344     }
1345
1346     @Override
1347     public void setEditor(@NotNull final HighlighterClient editor) {
1348       LOG.assertTrue(myEditor == null, "Highlighters cannot be reused with different editors");
1349       myEditor = editor;
1350     }
1351
1352     @Override
1353     public void setColorScheme(@NotNull EditorColorsScheme scheme) {
1354     }
1355   }
1356
1357   private static class MyTypedHandler extends TypedActionHandlerBase {
1358
1359     private MyTypedHandler(final TypedActionHandler originalAction) {
1360       super(originalAction);
1361     }
1362
1363     @Override
1364     public void execute(@NotNull final Editor editor, final char charTyped, @NotNull final DataContext dataContext) {
1365       final ConsoleViewImpl consoleView = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
1366       if (consoleView == null || !consoleView.myState.isRunning() || consoleView.myIsViewer) {
1367         if (myOriginalHandler != null) myOriginalHandler.execute(editor, charTyped, dataContext);
1368       }
1369       else {
1370         final String s = String.valueOf(charTyped);
1371         SelectionModel selectionModel = editor.getSelectionModel();
1372         if (selectionModel.hasSelection()) {
1373           consoleView.replaceUserText(s, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
1374         }
1375         else {
1376           consoleView.insertUserText(s, editor.getCaretModel().getOffset());
1377         }
1378       }
1379     }
1380   }
1381
1382   private abstract static class ConsoleAction extends AnAction implements DumbAware {
1383     @Override
1384     public void actionPerformed(final AnActionEvent e) {
1385       final DataContext context = e.getDataContext();
1386       final ConsoleViewImpl console = getRunningConsole(context);
1387       execute(console, context);
1388     }
1389
1390     protected abstract void execute(ConsoleViewImpl console, final DataContext context);
1391
1392     @Override
1393     public void update(final AnActionEvent e) {
1394       final ConsoleViewImpl console = getRunningConsole(e.getDataContext());
1395       e.getPresentation().setEnabled(console != null);
1396     }
1397
1398     @Nullable
1399     private static ConsoleViewImpl getRunningConsole(final DataContext context) {
1400       final Editor editor = CommonDataKeys.EDITOR.getData(context);
1401       if (editor != null) {
1402         final ConsoleViewImpl console = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
1403         if (console != null && console.myState.isRunning()) {
1404           return console;
1405         }
1406       }
1407       return null;
1408     }
1409   }
1410
1411   private static class EnterHandler extends ConsoleAction {
1412     @Override
1413     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1414       consoleView.print("\n", ConsoleViewContentType.USER_INPUT);
1415       consoleView.flushDeferredText();
1416       final Editor editor = consoleView.myEditor;
1417       editor.getCaretModel().moveToOffset(editor.getDocument().getTextLength());
1418       editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1419     }
1420   }
1421
1422   private static class PasteHandler extends ConsoleAction {
1423     @Override
1424     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1425       String s = CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor);
1426       if (s == null) return;
1427       s = StringUtil.convertLineSeparators(s);
1428       ApplicationManager.getApplication().assertIsDispatchThread();
1429       Editor editor = consoleView.myEditor;
1430       SelectionModel selectionModel = editor.getSelectionModel();
1431       if (selectionModel.hasSelection()) {
1432         consoleView.replaceUserText(s, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
1433       }
1434       else {
1435         consoleView.insertUserText(s, editor.getCaretModel().getOffset());
1436       }
1437     }
1438   }
1439
1440   private static class BackSpaceHandler extends ConsoleAction {
1441     @Override
1442     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1443       final Editor editor = consoleView.myEditor;
1444
1445       if (IncrementalSearchHandler.isHintVisible(editor)) {
1446         getDefaultActionHandler().execute(editor, context);
1447         return;
1448       }
1449
1450       final Document document = editor.getDocument();
1451       final int length = document.getTextLength();
1452       if (length == 0) {
1453         return;
1454       }
1455
1456       ApplicationManager.getApplication().assertIsDispatchThread();
1457
1458       SelectionModel selectionModel = editor.getSelectionModel();
1459       if (selectionModel.hasSelection()) {
1460         consoleView.deleteUserText(selectionModel.getSelectionStart(),
1461                                    selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
1462       }
1463       else if (editor.getCaretModel().getOffset() > 0) {
1464         consoleView.deleteUserText(editor.getCaretModel().getOffset() - 1, 1);
1465       }
1466     }
1467
1468     private static EditorActionHandler getDefaultActionHandler() {
1469       return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
1470     }
1471   }
1472
1473   private static class DeleteHandler extends ConsoleAction {
1474     @Override
1475     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1476       final Editor editor = consoleView.myEditor;
1477
1478       if (IncrementalSearchHandler.isHintVisible(editor)) {
1479         getDefaultActionHandler().execute(editor, context);
1480         return;
1481       }
1482
1483       final Document document = editor.getDocument();
1484       final int length = document.getTextLength();
1485       if (length == 0) {
1486         return;
1487       }
1488
1489       ApplicationManager.getApplication().assertIsDispatchThread();
1490       SelectionModel selectionModel = editor.getSelectionModel();
1491       if (selectionModel.hasSelection()) {
1492         consoleView.deleteUserText(selectionModel.getSelectionStart(),
1493                                    selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
1494       }
1495       else {
1496         consoleView.deleteUserText(editor.getCaretModel().getOffset(), 1);
1497       }
1498     }
1499
1500     private static EditorActionHandler getDefaultActionHandler() {
1501       return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
1502     }
1503   }
1504
1505   @Override
1506   public JComponent getPreferredFocusableComponent() {
1507     //ensure editor created
1508     getComponent();
1509     return myEditor.getContentComponent();
1510   }
1511
1512
1513   // navigate up/down in stack trace
1514   @Override
1515   public boolean hasNextOccurence() {
1516     return calcNextOccurrence(1) != null;
1517   }
1518
1519   @Override
1520   public boolean hasPreviousOccurence() {
1521     return calcNextOccurrence(-1) != null;
1522   }
1523
1524   @Override
1525   public OccurenceInfo goNextOccurence() {
1526     return calcNextOccurrence(1);
1527   }
1528
1529   @Nullable
1530   protected OccurenceInfo calcNextOccurrence(final int delta) {
1531     final EditorHyperlinkSupport hyperlinks = myHyperlinks;
1532     if (hyperlinks == null) {
1533       return null;
1534     }
1535
1536     return EditorHyperlinkSupport.getNextOccurrence(myEditor, delta, new Consumer<RangeHighlighter>() {
1537       @Override
1538       public void consume(RangeHighlighter next) {
1539         int offset = next.getStartOffset();
1540         scrollTo(offset);
1541         final HyperlinkInfo hyperlinkInfo = EditorHyperlinkSupport.getHyperlinkInfo(next);
1542         if (hyperlinkInfo instanceof BrowserHyperlinkInfo) {
1543           return;
1544         }
1545         if (hyperlinkInfo instanceof HyperlinkInfoBase) {
1546           VisualPosition position = myEditor.offsetToVisualPosition(offset);
1547           Point point = myEditor.visualPositionToXY(new VisualPosition(position.getLine() + 1, position.getColumn()));
1548           ((HyperlinkInfoBase)hyperlinkInfo).navigate(myProject, new RelativePoint(myEditor.getContentComponent(), point));
1549         }
1550         else if (hyperlinkInfo != null) {
1551           hyperlinkInfo.navigate(myProject);
1552         }
1553       }
1554     });
1555   }
1556
1557   @Override
1558   public OccurenceInfo goPreviousOccurence() {
1559     return calcNextOccurrence(-1);
1560   }
1561
1562   @Override
1563   public String getNextOccurenceActionName() {
1564     return ExecutionBundle.message("down.the.stack.trace");
1565   }
1566
1567   @Override
1568   public String getPreviousOccurenceActionName() {
1569     return ExecutionBundle.message("up.the.stack.trace");
1570   }
1571
1572   public void addCustomConsoleAction(@NotNull AnAction action) {
1573     customActions.add(action);
1574   }
1575
1576   @Override
1577   @NotNull
1578   public AnAction[] createConsoleActions() {
1579     //Initializing prev and next occurrences actions
1580     final CommonActionsManager actionsManager = CommonActionsManager.getInstance();
1581     final AnAction prevAction = actionsManager.createPrevOccurenceAction(this);
1582     prevAction.getTemplatePresentation().setText(getPreviousOccurenceActionName());
1583     final AnAction nextAction = actionsManager.createNextOccurenceAction(this);
1584     nextAction.getTemplatePresentation().setText(getNextOccurenceActionName());
1585
1586     final AnAction switchSoftWrapsAction = new ToggleUseSoftWrapsToolbarAction(SoftWrapAppliancePlaces.CONSOLE) {
1587
1588       /**
1589        * There is a possible case that more than console is open and user toggles soft wraps mode at one of them. We want
1590        * to update another console(s) representation as well when they are switched on after that. Hence, we remember last
1591        * used soft wraps mode and perform update if we see that the current value differs from the stored.
1592        */
1593       private boolean myLastIsSelected;
1594
1595       @Override
1596       protected Editor getEditor(AnActionEvent e) {
1597         return myEditor;
1598       }
1599
1600       @Override
1601       public boolean isSelected(AnActionEvent e) {
1602         boolean result = super.isSelected(e);
1603         if (result ^ myLastIsSelected) {
1604           setSelected(null, result);
1605         }
1606         return myLastIsSelected = result;
1607       }
1608
1609       @Override
1610       public void setSelected(AnActionEvent e, final boolean state) {
1611         super.setSelected(e, state);
1612         if (myEditor == null) {
1613           return;
1614         }
1615
1616         final String placeholder = myCommandLineFolding.getPlaceholder(0);
1617         final FoldingModel foldingModel = myEditor.getFoldingModel();
1618         final int firstLineEnd = myEditor.getDocument().getLineEndOffset(0);
1619         foldingModel.runBatchFoldingOperation(new Runnable() {
1620           @Override
1621           public void run() {
1622             FoldRegion[] regions = foldingModel.getAllFoldRegions();
1623             if (regions.length > 0 && regions[0].getStartOffset() == 0 && regions[0].getEndOffset() == firstLineEnd) {
1624               foldingModel.removeFoldRegion(regions[0]);
1625             }
1626             if (placeholder != null) {
1627               FoldRegion foldRegion = foldingModel.addFoldRegion(0, firstLineEnd, placeholder);
1628               if (foldRegion != null) {
1629                 foldRegion.setExpanded(false);
1630               }
1631             }
1632           }
1633         });
1634       }
1635     };
1636     final AnAction autoScrollToTheEndAction = new ScrollToTheEndToolbarAction(myEditor);
1637
1638     //Initializing custom actions
1639     final AnAction[] consoleActions = new AnAction[6 + customActions.size()];
1640     consoleActions[0] = prevAction;
1641     consoleActions[1] = nextAction;
1642     consoleActions[2] = switchSoftWrapsAction;
1643     consoleActions[3] = autoScrollToTheEndAction;
1644     consoleActions[4] = ActionManager.getInstance().getAction("Print");
1645     consoleActions[5] = new ClearAllAction(this);
1646     for (int i = 0; i < customActions.size(); ++i) {
1647       consoleActions[i + 6] = customActions.get(i);
1648     }
1649     ConsoleActionsPostProcessor[] postProcessors = Extensions.getExtensions(ConsoleActionsPostProcessor.EP_NAME);
1650     AnAction[] result = consoleActions;
1651     for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
1652       result = postProcessor.postProcess(this, result);
1653     }
1654     return result;
1655   }
1656
1657   @Override
1658   public void allowHeavyFilters() {
1659     myAllowHeavyFilters = true;
1660   }
1661
1662   @Override
1663   public void addChangeListener(@NotNull final ChangeListener listener, @NotNull final Disposable parent) {
1664     myListeners.add(listener);
1665     Disposer.register(parent, new Disposable() {
1666       @Override
1667       public void dispose() {
1668         myListeners.remove(listener);
1669       }
1670     });
1671   }
1672
1673   /**
1674    * insert text to document
1675    *
1676    * @param s      inserted text
1677    * @param offset relatively to all document text
1678    */
1679   private void insertUserText(final String s, int offset) {
1680     ApplicationManager.getApplication().assertIsDispatchThread();
1681     final ConsoleViewImpl consoleView = this;
1682     final ConsoleBuffer buffer = consoleView.myBuffer;
1683     final Editor editor = consoleView.myEditor;
1684     final Document document = editor.getDocument();
1685     final int startOffset;
1686
1687     String textToUse = StringUtil.convertLineSeparators(s);
1688     synchronized (consoleView.LOCK) {
1689       if (consoleView.myTokens.isEmpty()) {
1690         addToken(0, null, ConsoleViewContentType.SYSTEM_OUTPUT);
1691       }
1692       final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1693       if (info.contentType != ConsoleViewContentType.USER_INPUT && !StringUtil.containsChar(textToUse, '\n')) {
1694         consoleView.print(textToUse, ConsoleViewContentType.USER_INPUT);
1695         consoleView.flushDeferredText();
1696         editor.getCaretModel().moveToOffset(document.getTextLength());
1697         editor.getSelectionModel().removeSelection();
1698         return;
1699       }
1700       if (info.contentType != ConsoleViewContentType.USER_INPUT) {
1701         insertUserText("temp", offset);
1702         final TokenInfo newInfo = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1703         replaceUserText(textToUse, newInfo.startOffset, newInfo.endOffset);
1704         return;
1705       }
1706
1707       final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
1708       if (offset > info.endOffset) {
1709         startOffset = info.endOffset;
1710       }
1711       else {
1712         startOffset = Math.max(deferredOffset, Math.max(info.startOffset, offset));
1713       }
1714
1715       buffer.addUserText(startOffset - deferredOffset, textToUse);
1716
1717       int charCountToAdd = textToUse.length();
1718       info.endOffset += charCountToAdd;
1719       consoleView.myContentSize += charCountToAdd;
1720     }
1721
1722     try {
1723       myInDocumentUpdate = true;
1724       document.insertString(startOffset, textToUse);
1725     }
1726     finally {
1727       myInDocumentUpdate = false;
1728     }
1729     // Math.max is needed when cyclic buffer is used
1730     editor.getCaretModel().moveToOffset(Math.min(startOffset + textToUse.length(), document.getTextLength()));
1731     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1732   }
1733
1734   /**
1735    * replace text
1736    *
1737    * @param s     text for replace
1738    * @param start relatively to all document text
1739    * @param end   relatively to all document text
1740    */
1741   private void replaceUserText(final String s, int start, int end) {
1742     if (start == end) {
1743       insertUserText(s, start);
1744       return;
1745     }
1746     final ConsoleViewImpl consoleView = this;
1747     final ConsoleBuffer buffer = consoleView.myBuffer;
1748     final Editor editor = consoleView.myEditor;
1749     final Document document = editor.getDocument();
1750     final int startOffset;
1751     final int endOffset;
1752
1753     synchronized (consoleView.LOCK) {
1754       if (consoleView.myTokens.isEmpty()) return;
1755       final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1756       if (info.contentType != ConsoleViewContentType.USER_INPUT) {
1757         consoleView.print(s, ConsoleViewContentType.USER_INPUT);
1758         consoleView.flushDeferredText();
1759         editor.getCaretModel().moveToOffset(document.getTextLength());
1760         editor.getSelectionModel().removeSelection();
1761         return;
1762       }
1763       if (buffer.getUserInputLength() <= 0) return;
1764
1765       final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
1766
1767       startOffset = getStartOffset(start, info, deferredOffset);
1768       endOffset = getEndOffset(end, info);
1769
1770       if (startOffset == -1 ||
1771           endOffset == -1 ||
1772           endOffset <= startOffset) {
1773         editor.getSelectionModel().removeSelection();
1774         editor.getCaretModel().moveToOffset(start);
1775         return;
1776       }
1777       int charCountToReplace = s.length() - endOffset + startOffset;
1778
1779       buffer.replaceUserText(startOffset - deferredOffset, endOffset - deferredOffset, s);
1780
1781       info.endOffset += charCountToReplace;
1782       if (info.startOffset == info.endOffset) {
1783         consoleView.myTokens.remove(consoleView.myTokens.size() - 1);
1784       }
1785       consoleView.myContentSize += charCountToReplace;
1786     }
1787
1788     try {
1789       myInDocumentUpdate = true;
1790       document.replaceString(startOffset, endOffset, s);
1791     }
1792     finally {
1793       myInDocumentUpdate = false;
1794     }
1795     editor.getCaretModel().moveToOffset(startOffset + s.length());
1796     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1797     editor.getSelectionModel().removeSelection();
1798   }
1799
1800   /**
1801    * delete text
1802    *
1803    * @param offset relatively to all document text
1804    * @param length length of deleted text
1805    */
1806   private void deleteUserText(int offset, int length) {
1807     ConsoleViewImpl consoleView = this;
1808     ConsoleBuffer buffer = consoleView.myBuffer;
1809     final Editor editor = consoleView.myEditor;
1810     final Document document = editor.getDocument();
1811     final int startOffset;
1812     final int endOffset;
1813
1814     synchronized (consoleView.LOCK) {
1815       if (consoleView.myTokens.isEmpty()) return;
1816       final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1817       if (info.contentType != ConsoleViewContentType.USER_INPUT) return;
1818       if (myBuffer.getUserInputLength() == 0) return;
1819
1820       final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
1821       startOffset = getStartOffset(offset, info, deferredOffset);
1822       endOffset = getEndOffset(offset + length, info);
1823       if (startOffset == -1 ||
1824           endOffset == -1 ||
1825           endOffset <= startOffset ||
1826           startOffset < deferredOffset) {
1827         editor.getSelectionModel().removeSelection();
1828         editor.getCaretModel().moveToOffset(offset);
1829         return;
1830       }
1831
1832       buffer.removeUserText(startOffset - deferredOffset, endOffset - deferredOffset);
1833     }
1834
1835     try {
1836       myInDocumentUpdate = true;
1837       document.deleteString(startOffset, endOffset);
1838     }
1839     finally {
1840       myInDocumentUpdate = false;
1841     }
1842     editor.getCaretModel().moveToOffset(startOffset);
1843     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1844     editor.getSelectionModel().removeSelection();
1845   }
1846
1847   //util methods for add, replace, delete methods
1848   private static int getStartOffset(int offset, TokenInfo info, int deferredOffset) {
1849     int startOffset;
1850     if (offset >= info.startOffset && offset < info.endOffset) {
1851       startOffset = Math.max(offset, deferredOffset);
1852     }
1853     else if (offset < info.startOffset) {
1854       startOffset = Math.max(info.startOffset, deferredOffset);
1855     }
1856     else {
1857       startOffset = -1;
1858     }
1859     return startOffset;
1860   }
1861
1862   private static int getEndOffset(int offset, TokenInfo info) {
1863     int endOffset;
1864     if (offset > info.endOffset) {
1865       endOffset = info.endOffset;
1866     }
1867     else if (offset <= info.startOffset) {
1868       endOffset = -1;
1869     }
1870     else {
1871       endOffset = offset;
1872     }
1873     return endOffset;
1874   }
1875
1876   public boolean isRunning() {
1877     return myState.isRunning();
1878   }
1879
1880   /**
1881    * Command line used to launch application/test from idea may be quite long.
1882    * Hence, it takes many visual lines during representation if soft wraps are enabled
1883    * or, otherwise, takes many columns and makes horizontal scrollbar thumb too small.
1884    * <p/>
1885    * Our point is to fold such long command line and represent it as a single visual line by default.
1886    */
1887   private class CommandLineFolding extends ConsoleFolding {
1888
1889     /**
1890      * Checks if target line should be folded and returns its placeholder if the examination succeeds.
1891      *
1892      * @param line index of line to check
1893      * @return placeholder text if given line should be folded; <code>null</code> otherwise
1894      */
1895     @Nullable
1896     private String getPlaceholder(int line) {
1897       if (myEditor == null || line != 0) {
1898         return null;
1899       }
1900
1901       String text = EditorHyperlinkSupport.getLineText(myEditor.getDocument(), 0, false);
1902       // Don't fold the first line if the line is not that big.
1903       if (text.length() < 1000) {
1904         return null;
1905       }
1906       int index = 0;
1907       if (text.charAt(0) == '"') {
1908         index = text.indexOf('"', 1) + 1;
1909       }
1910       if (index == 0) {
1911         boolean nonWhiteSpaceFound = false;
1912         for (; index < text.length(); index++) {
1913           char c = text.charAt(index);
1914           if (c != ' ' && c != '\t') {
1915             nonWhiteSpaceFound = true;
1916             continue;
1917           }
1918           if (nonWhiteSpaceFound) {
1919             break;
1920           }
1921         }
1922       }
1923       assert index <= text.length();
1924       return text.substring(0, index) + " ...";
1925     }
1926
1927     @Override
1928     public boolean shouldFoldLine(String line) {
1929       return false;
1930     }
1931
1932     @Override
1933     public String getPlaceholderText(List<String> lines) {
1934       // Is not expected to be called.
1935       return "<...>";
1936     }
1937   }
1938
1939   private class MyFlushRunnable implements Runnable {
1940     private volatile boolean myValid = true;
1941     @Override
1942     public final void run() {
1943       synchronized (myCurrentRequests) {
1944         myCurrentRequests.remove(this);
1945       }
1946       if (myValid) {
1947         doRun();
1948       }
1949     }
1950
1951     protected void doRun() {
1952       flushDeferredText();
1953     }
1954
1955     public void invalidate() {
1956       myValid = false;
1957     }
1958
1959     public boolean isValid() {
1960       return myValid;
1961     }
1962
1963     @Override
1964     public boolean equals(Object o) {
1965       if (this == o) return true;
1966       if (o == null || getClass() != o.getClass()) return false;
1967
1968       MyFlushRunnable runnable = (MyFlushRunnable)o;
1969
1970       return myValid == runnable.myValid;
1971     }
1972
1973     @Override
1974     public int hashCode() {
1975       return getClass().hashCode();
1976     }
1977   }
1978
1979   private final class MyClearRunnable extends MyFlushRunnable {
1980     @Override
1981     public void doRun() {
1982       flushDeferredText(true);
1983     }
1984   }
1985
1986   @NotNull
1987   public Project getProject() {
1988     return myProject;
1989   }
1990
1991   private class HyperlinkNavigationAction extends DumbAwareAction {
1992     @Override
1993     public void actionPerformed(AnActionEvent e) {
1994       Runnable runnable = myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition());
1995       assert runnable != null;
1996       runnable.run();
1997     }
1998
1999     @Override
2000     public void update(AnActionEvent e) {
2001       e.getPresentation().setEnabled(myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition()) != null);
2002     }
2003   }
2004
2005   @NotNull
2006   public String getText() {
2007     return myEditor.getDocument().getText();
2008   }
2009 }
2010