enable in-place replace action for Analyze Stacktrace (IDEA-CR-9987)
[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         if (ConsoleViewUtil.isReplaceActionEnabledForConsoleViewEditor(myEditor)) {
996           clearHyperlinkAndFoldings();
997           highlightHyperlinksAndFoldings(event.getDocument().createRangeMarker(0, 0));
998         }
999         else {
1000           LOG.warn("unhandled external change: " + event);
1001         }
1002       }
1003     }
1004   }
1005
1006   protected EditorEx doCreateConsoleEditor() {
1007     return ConsoleViewUtil.setupConsoleEditor(myProject, true, false);
1008   }
1009
1010   protected MyHighlighter createHighlighter() {
1011     return new MyHighlighter();
1012   }
1013
1014   private void registerConsoleEditorActions() {
1015     Shortcut[] shortcuts = KeymapManager.getInstance().getActiveKeymap().getShortcuts(IdeActions.ACTION_GOTO_DECLARATION);
1016     CustomShortcutSet shortcutSet = new CustomShortcutSet(ArrayUtil.mergeArrays(shortcuts, CommonShortcuts.ENTER.getShortcuts()));
1017     new HyperlinkNavigationAction().registerCustomShortcutSet(shortcutSet, myEditor.getContentComponent());
1018
1019
1020     if (!myIsViewer) {
1021       new EnterHandler().registerCustomShortcutSet(CommonShortcuts.ENTER, myEditor.getContentComponent());
1022       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_PASTE, new PasteHandler());
1023       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_BACKSPACE, new BackSpaceHandler());
1024       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_DELETE, new DeleteHandler());
1025
1026       registerActionHandler(myEditor, EOFAction.ACTION_ID);
1027     }
1028   }
1029
1030   private static void registerActionHandler(final Editor editor, final String actionId) {
1031     AnAction action = ActionManager.getInstance().getAction(actionId);
1032     action.registerCustomShortcutSet(action.getShortcutSet(), editor.getContentComponent());
1033   }
1034
1035   private static void registerActionHandler(final Editor editor, final String actionId, final AnAction action) {
1036     final Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
1037     final Shortcut[] shortcuts = keymap.getShortcuts(actionId);
1038     action.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), editor.getContentComponent());
1039   }
1040
1041   private void popupInvoked(MouseEvent mouseEvent) {
1042     final ActionManager actionManager = ActionManager.getInstance();
1043     final HyperlinkInfo info = myHyperlinks != null ? myHyperlinks.getHyperlinkInfoByPoint(mouseEvent.getPoint()) : null;
1044     ActionGroup group = null;
1045     if (info instanceof HyperlinkWithPopupMenuInfo) {
1046       group = ((HyperlinkWithPopupMenuInfo)info).getPopupMenuGroup(mouseEvent);
1047     }
1048     if (group == null) {
1049       group = (ActionGroup)actionManager.getAction(CONSOLE_VIEW_POPUP_MENU);
1050     }
1051     final ConsoleActionsPostProcessor[] postProcessors = Extensions.getExtensions(ConsoleActionsPostProcessor.EP_NAME);
1052     AnAction[] result = group.getChildren(null);
1053
1054     for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
1055       result = postProcessor.postProcessPopupActions(this, result);
1056     }
1057     final DefaultActionGroup processedGroup = new DefaultActionGroup(result);
1058     final ActionPopupMenu menu = actionManager.createActionPopupMenu(ActionPlaces.EDITOR_POPUP, processedGroup);
1059     menu.getComponent().show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
1060   }
1061
1062   private void highlightHyperlinksAndFoldings(RangeMarker lastProcessedOutput) {
1063     boolean canHighlightHyperlinks = !myFilters.isEmpty();
1064
1065     if (!canHighlightHyperlinks && myUpdateFoldingsEnabled) {
1066       return;
1067     }
1068     final int line1 = lastProcessedOutput.isValid() ? myEditor.getDocument().getLineNumber(lastProcessedOutput.getEndOffset()) : 0;
1069     lastProcessedOutput.dispose();
1070     int endLine = myEditor.getDocument().getLineCount() - 1;
1071     ApplicationManager.getApplication().assertIsDispatchThread();
1072
1073     if (canHighlightHyperlinks) {
1074       myHyperlinks.highlightHyperlinks(myFilters, line1, endLine);
1075     }
1076
1077     if (myAllowHeavyFilters && myFilters.isAnyHeavy() && myFilters.shouldRunHeavy()) {
1078       runHeavyFilters(line1, endLine);
1079     }
1080     if (myUpdateFoldingsEnabled) {
1081       updateFoldings(line1, endLine, true);
1082     }
1083   }
1084
1085   private void runHeavyFilters(int line1, int endLine) {
1086     final int startLine = Math.max(0, line1);
1087
1088     final Document document = myEditor.getDocument();
1089     final int startOffset = document.getLineStartOffset(startLine);
1090     String text = document.getText(new TextRange(startOffset, document.getLineEndOffset(endLine)));
1091     final Document documentCopy = new DocumentImpl(text,true);
1092     documentCopy.setReadOnly(true);
1093
1094     myJLayeredPane.startUpdating();
1095     final int currentValue = myHeavyUpdateTicket;
1096     assert myHeavyAlarm != null;
1097     myHeavyAlarm.addRequest(new Runnable() {
1098       @Override
1099       public void run() {
1100         if (!myFilters.shouldRunHeavy()) return;
1101         try {
1102           myFilters.applyHeavyFilter(documentCopy, startOffset, startLine, new Consumer<FilterMixin.AdditionalHighlight>() {
1103             @Override
1104             public void consume(final FilterMixin.AdditionalHighlight additionalHighlight) {
1105               addFlushRequest(new MyFlushRunnable() {
1106                 @Override
1107                 public void doRun() {
1108                   if (myHeavyUpdateTicket != currentValue) return;
1109                   myHyperlinks.addHighlighter(additionalHighlight.getStart(), additionalHighlight.getEnd(),
1110                                               additionalHighlight.getTextAttributes(null));
1111                 }
1112
1113                 @Override
1114                 public boolean equals(Object o) {
1115                   return this == o && super.equals(o);
1116                 }
1117               });
1118             }
1119           });
1120         }
1121         finally {
1122           if (myHeavyAlarm.isEmpty()) {
1123             SwingUtilities.invokeLater(new Runnable() {
1124               @Override
1125               public void run() {
1126                 myJLayeredPane.finishUpdating();
1127               }
1128             });
1129           }
1130         }
1131       }
1132     }, 0);
1133   }
1134
1135   private void updateFoldings(final int line1, final int endLine, boolean immediately) {
1136     final Document document = myEditor.getDocument();
1137     final CharSequence chars = document.getCharsSequence();
1138     final int startLine = Math.max(0, line1);
1139     final List<FoldRegion> toAdd = new ArrayList<FoldRegion>();
1140     for (int line = startLine; line <= endLine; line++) {
1141       boolean flushOnly = line == endLine;
1142       /*
1143       Grep Console plugin allows to fold empty lines. We need to handle this case in a special way.
1144
1145       Multiple lines are grouped into one folding, but to know when you can create the folding,
1146       you need a line which does not belong to that folding.
1147       When a new line, or a chunk of lines is printed, #addFolding is called for that lines + for an empty string
1148       (which basically does only one thing, gets a folding displayed).
1149       We do not want to process that empty string, but also we do not want to wait for another line
1150       which will create and display the folding - we'd see an unfolded stacktrace until another text came and flushed it.
1151       So therefore the condition, the last line(empty string) should still flush, but not be processed by
1152       com.intellij.execution.ConsoleFolding.
1153        */
1154       addFolding(document, chars, line, toAdd, flushOnly);
1155     }
1156     if (!toAdd.isEmpty()) {
1157       doUpdateFolding(toAdd, immediately);
1158     }
1159   }
1160
1161   private void doUpdateFolding(final List<FoldRegion> toAdd, final boolean immediately) {
1162     assertIsDispatchThread();
1163     myPendingFoldRegions.addAll(toAdd);
1164
1165     myFoldingAlarm.cancelAllRequests();
1166     final Runnable runnable = new Runnable() {
1167       @Override
1168       public void run() {
1169         if (myEditor == null || myEditor.isDisposed()) {
1170           return;
1171         }
1172
1173         assertIsDispatchThread();
1174         final FoldingModel model = myEditor.getFoldingModel();
1175         final Runnable operation = new Runnable() {
1176           @Override
1177           public void run() {
1178             assertIsDispatchThread();
1179             for (FoldRegion region : myPendingFoldRegions) {
1180               region.setExpanded(false);
1181               model.addFoldRegion(region);
1182             }
1183             myPendingFoldRegions.clear();
1184           }
1185         };
1186         if (immediately) {
1187           model.runBatchFoldingOperation(operation);
1188         }
1189         else {
1190           model.runBatchFoldingOperationDoNotCollapseCaret(operation);
1191         }
1192       }
1193     };
1194     if (immediately || myPendingFoldRegions.size() > 100) {
1195       runnable.run();
1196     }
1197     else {
1198       myFoldingAlarm.addRequest(runnable, 50);
1199     }
1200   }
1201
1202   private void addFolding(Document document, CharSequence chars, int line, List<FoldRegion> toAdd, boolean flushOnly) {
1203     ConsoleFolding current = null;
1204     if (!flushOnly) {
1205       String commandLinePlaceholder = myCommandLineFolding.getPlaceholder(line);
1206       if (commandLinePlaceholder != null) {
1207         FoldRegion region = myEditor.getFoldingModel()
1208           .createFoldRegion(document.getLineStartOffset(line), document.getLineEndOffset(line), commandLinePlaceholder, null, false);
1209         toAdd.add(region);
1210         return;
1211       }
1212       current = foldingForLine(EditorHyperlinkSupport.getLineText(document, line, false));
1213       if (current != null) {
1214         myFolding.put(line, current);
1215       }
1216     }
1217
1218     final ConsoleFolding prevFolding = myFolding.get(line - 1);
1219     if (current == null && prevFolding != null) {
1220       final int lEnd = line - 1;
1221       int lStart = lEnd;
1222       while (prevFolding.equals(myFolding.get(lStart - 1))) lStart--;
1223
1224       for (int i = lStart; i <= lEnd; i++) {
1225         myFolding.remove(i);
1226       }
1227
1228       List<String> toFold = new ArrayList<String>(lEnd - lStart + 1);
1229       for (int i = lStart; i <= lEnd; i++) {
1230         toFold.add(EditorHyperlinkSupport.getLineText(document, i, false));
1231       }
1232
1233       int oStart = document.getLineStartOffset(lStart);
1234       if (oStart > 0) oStart--;
1235       int oEnd = CharArrayUtil.shiftBackward(chars, document.getLineEndOffset(lEnd) - 1, " \t") + 1;
1236
1237       String placeholder = prevFolding.getPlaceholderText(toFold);
1238       FoldRegion region = placeholder == null ? null : myEditor.getFoldingModel().createFoldRegion(oStart, oEnd, placeholder, null, false);
1239       if (region != null) {
1240         toAdd.add(region);
1241       }
1242     }
1243   }
1244
1245   @Nullable
1246   private static ConsoleFolding foldingForLine(String lineText) {
1247     for (ConsoleFolding folding : ConsoleFolding.EP_NAME.getExtensions()) {
1248       if (folding.shouldFoldLine(lineText)) {
1249         return folding;
1250       }
1251     }
1252     return null;
1253   }
1254
1255
1256   public static class ClearAllAction extends DumbAwareAction {
1257     private final ConsoleView myConsoleView;
1258
1259     @SuppressWarnings("unused")
1260     public ClearAllAction() {
1261       this(null);
1262     }
1263
1264     public ClearAllAction(ConsoleView consoleView) {
1265       super(ExecutionBundle.message("clear.all.from.console.action.name"), "Clear the contents of the console", AllIcons.Actions.GC);
1266       myConsoleView = consoleView;
1267     }
1268
1269     @Override
1270     public void update(AnActionEvent e) {
1271       boolean enabled = myConsoleView != null && myConsoleView.getContentSize() > 0;
1272       if (!enabled) {
1273         enabled = e.getData(LangDataKeys.CONSOLE_VIEW) != null;
1274         Editor editor = e.getData(CommonDataKeys.EDITOR);
1275         if (editor != null && editor.getDocument().getTextLength() == 0) {
1276           enabled = false;
1277         }
1278       }
1279       e.getPresentation().setEnabled(enabled);
1280     }
1281
1282     @Override
1283     public void actionPerformed(final AnActionEvent e) {
1284       final ConsoleView consoleView = myConsoleView != null ? myConsoleView : e.getData(LangDataKeys.CONSOLE_VIEW);
1285       if (consoleView != null) {
1286         consoleView.clear();
1287       }
1288     }
1289   }
1290
1291   private class MyHighlighter extends DocumentAdapter implements EditorHighlighter {
1292     private HighlighterClient myEditor;
1293
1294     @NotNull
1295     @Override
1296     public HighlighterIterator createIterator(final int startOffset) {
1297       final int startIndex = ConsoleUtil.findTokenInfoIndexByOffset(myTokens, startOffset);
1298
1299       return new HighlighterIterator() {
1300         private int myIndex = startIndex;
1301
1302         @Override
1303         public TextAttributes getTextAttributes() {
1304           return atEnd() ? null : getTokenInfo().contentType.getAttributes();
1305         }
1306
1307         @Override
1308         public int getStart() {
1309           return atEnd() ? 0 : getTokenInfo().startOffset;
1310         }
1311
1312         @Override
1313         public int getEnd() {
1314           return atEnd() ? 0 : getTokenInfo().endOffset;
1315         }
1316
1317         @Override
1318         public IElementType getTokenType() {
1319           return null;
1320         }
1321
1322         @Override
1323         public void advance() {
1324           myIndex++;
1325         }
1326
1327         @Override
1328         public void retreat() {
1329           myIndex--;
1330         }
1331
1332         @Override
1333         public boolean atEnd() {
1334           return myIndex < 0 || myIndex >= myTokens.size();
1335         }
1336
1337         @Override
1338         public Document getDocument() {
1339           return myEditor.getDocument();
1340         }
1341
1342         private TokenInfo getTokenInfo() {
1343           return myTokens.get(myIndex);
1344         }
1345       };
1346     }
1347
1348     @Override
1349     public void setText(@NotNull final CharSequence text) {
1350     }
1351
1352     @Override
1353     public void setEditor(@NotNull final HighlighterClient editor) {
1354       LOG.assertTrue(myEditor == null, "Highlighters cannot be reused with different editors");
1355       myEditor = editor;
1356     }
1357
1358     @Override
1359     public void setColorScheme(@NotNull EditorColorsScheme scheme) {
1360     }
1361   }
1362
1363   private static class MyTypedHandler extends TypedActionHandlerBase {
1364
1365     private MyTypedHandler(final TypedActionHandler originalAction) {
1366       super(originalAction);
1367     }
1368
1369     @Override
1370     public void execute(@NotNull final Editor editor, final char charTyped, @NotNull final DataContext dataContext) {
1371       final ConsoleViewImpl consoleView = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
1372       if (consoleView == null || !consoleView.myState.isRunning() || consoleView.myIsViewer) {
1373         if (myOriginalHandler != null) myOriginalHandler.execute(editor, charTyped, dataContext);
1374       }
1375       else {
1376         final String s = String.valueOf(charTyped);
1377         SelectionModel selectionModel = editor.getSelectionModel();
1378         if (selectionModel.hasSelection()) {
1379           consoleView.replaceUserText(s, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
1380         }
1381         else {
1382           consoleView.insertUserText(s, editor.getCaretModel().getOffset());
1383         }
1384       }
1385     }
1386   }
1387
1388   private abstract static class ConsoleAction extends AnAction implements DumbAware {
1389     @Override
1390     public void actionPerformed(final AnActionEvent e) {
1391       final DataContext context = e.getDataContext();
1392       final ConsoleViewImpl console = getRunningConsole(context);
1393       execute(console, context);
1394     }
1395
1396     protected abstract void execute(ConsoleViewImpl console, final DataContext context);
1397
1398     @Override
1399     public void update(final AnActionEvent e) {
1400       final ConsoleViewImpl console = getRunningConsole(e.getDataContext());
1401       e.getPresentation().setEnabled(console != null);
1402     }
1403
1404     @Nullable
1405     private static ConsoleViewImpl getRunningConsole(final DataContext context) {
1406       final Editor editor = CommonDataKeys.EDITOR.getData(context);
1407       if (editor != null) {
1408         final ConsoleViewImpl console = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
1409         if (console != null && console.myState.isRunning()) {
1410           return console;
1411         }
1412       }
1413       return null;
1414     }
1415   }
1416
1417   private static class EnterHandler extends ConsoleAction {
1418     @Override
1419     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1420       consoleView.print("\n", ConsoleViewContentType.USER_INPUT);
1421       consoleView.flushDeferredText();
1422       final Editor editor = consoleView.myEditor;
1423       editor.getCaretModel().moveToOffset(editor.getDocument().getTextLength());
1424       editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1425     }
1426   }
1427
1428   private static class PasteHandler extends ConsoleAction {
1429     @Override
1430     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1431       String s = CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor);
1432       if (s == null) return;
1433       s = StringUtil.convertLineSeparators(s);
1434       ApplicationManager.getApplication().assertIsDispatchThread();
1435       Editor editor = consoleView.myEditor;
1436       SelectionModel selectionModel = editor.getSelectionModel();
1437       if (selectionModel.hasSelection()) {
1438         consoleView.replaceUserText(s, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
1439       }
1440       else {
1441         consoleView.insertUserText(s, editor.getCaretModel().getOffset());
1442       }
1443     }
1444   }
1445
1446   private static class BackSpaceHandler extends ConsoleAction {
1447     @Override
1448     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1449       final Editor editor = consoleView.myEditor;
1450
1451       if (IncrementalSearchHandler.isHintVisible(editor)) {
1452         getDefaultActionHandler().execute(editor, context);
1453         return;
1454       }
1455
1456       final Document document = editor.getDocument();
1457       final int length = document.getTextLength();
1458       if (length == 0) {
1459         return;
1460       }
1461
1462       ApplicationManager.getApplication().assertIsDispatchThread();
1463
1464       SelectionModel selectionModel = editor.getSelectionModel();
1465       if (selectionModel.hasSelection()) {
1466         consoleView.deleteUserText(selectionModel.getSelectionStart(),
1467                                    selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
1468       }
1469       else if (editor.getCaretModel().getOffset() > 0) {
1470         consoleView.deleteUserText(editor.getCaretModel().getOffset() - 1, 1);
1471       }
1472     }
1473
1474     private static EditorActionHandler getDefaultActionHandler() {
1475       return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
1476     }
1477   }
1478
1479   private static class DeleteHandler extends ConsoleAction {
1480     @Override
1481     public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
1482       final Editor editor = consoleView.myEditor;
1483
1484       if (IncrementalSearchHandler.isHintVisible(editor)) {
1485         getDefaultActionHandler().execute(editor, context);
1486         return;
1487       }
1488
1489       final Document document = editor.getDocument();
1490       final int length = document.getTextLength();
1491       if (length == 0) {
1492         return;
1493       }
1494
1495       ApplicationManager.getApplication().assertIsDispatchThread();
1496       SelectionModel selectionModel = editor.getSelectionModel();
1497       if (selectionModel.hasSelection()) {
1498         consoleView.deleteUserText(selectionModel.getSelectionStart(),
1499                                    selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
1500       }
1501       else {
1502         consoleView.deleteUserText(editor.getCaretModel().getOffset(), 1);
1503       }
1504     }
1505
1506     private static EditorActionHandler getDefaultActionHandler() {
1507       return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
1508     }
1509   }
1510
1511   @Override
1512   public JComponent getPreferredFocusableComponent() {
1513     //ensure editor created
1514     getComponent();
1515     return myEditor.getContentComponent();
1516   }
1517
1518
1519   // navigate up/down in stack trace
1520   @Override
1521   public boolean hasNextOccurence() {
1522     return calcNextOccurrence(1) != null;
1523   }
1524
1525   @Override
1526   public boolean hasPreviousOccurence() {
1527     return calcNextOccurrence(-1) != null;
1528   }
1529
1530   @Override
1531   public OccurenceInfo goNextOccurence() {
1532     return calcNextOccurrence(1);
1533   }
1534
1535   @Nullable
1536   protected OccurenceInfo calcNextOccurrence(final int delta) {
1537     final EditorHyperlinkSupport hyperlinks = myHyperlinks;
1538     if (hyperlinks == null) {
1539       return null;
1540     }
1541
1542     return EditorHyperlinkSupport.getNextOccurrence(myEditor, delta, new Consumer<RangeHighlighter>() {
1543       @Override
1544       public void consume(RangeHighlighter next) {
1545         int offset = next.getStartOffset();
1546         scrollTo(offset);
1547         final HyperlinkInfo hyperlinkInfo = EditorHyperlinkSupport.getHyperlinkInfo(next);
1548         if (hyperlinkInfo instanceof BrowserHyperlinkInfo) {
1549           return;
1550         }
1551         if (hyperlinkInfo instanceof HyperlinkInfoBase) {
1552           VisualPosition position = myEditor.offsetToVisualPosition(offset);
1553           Point point = myEditor.visualPositionToXY(new VisualPosition(position.getLine() + 1, position.getColumn()));
1554           ((HyperlinkInfoBase)hyperlinkInfo).navigate(myProject, new RelativePoint(myEditor.getContentComponent(), point));
1555         }
1556         else if (hyperlinkInfo != null) {
1557           hyperlinkInfo.navigate(myProject);
1558         }
1559       }
1560     });
1561   }
1562
1563   @Override
1564   public OccurenceInfo goPreviousOccurence() {
1565     return calcNextOccurrence(-1);
1566   }
1567
1568   @Override
1569   public String getNextOccurenceActionName() {
1570     return ExecutionBundle.message("down.the.stack.trace");
1571   }
1572
1573   @Override
1574   public String getPreviousOccurenceActionName() {
1575     return ExecutionBundle.message("up.the.stack.trace");
1576   }
1577
1578   public void addCustomConsoleAction(@NotNull AnAction action) {
1579     customActions.add(action);
1580   }
1581
1582   @Override
1583   @NotNull
1584   public AnAction[] createConsoleActions() {
1585     //Initializing prev and next occurrences actions
1586     final CommonActionsManager actionsManager = CommonActionsManager.getInstance();
1587     final AnAction prevAction = actionsManager.createPrevOccurenceAction(this);
1588     prevAction.getTemplatePresentation().setText(getPreviousOccurenceActionName());
1589     final AnAction nextAction = actionsManager.createNextOccurenceAction(this);
1590     nextAction.getTemplatePresentation().setText(getNextOccurenceActionName());
1591
1592     final AnAction switchSoftWrapsAction = new ToggleUseSoftWrapsToolbarAction(SoftWrapAppliancePlaces.CONSOLE) {
1593
1594       /**
1595        * There is a possible case that more than console is open and user toggles soft wraps mode at one of them. We want
1596        * to update another console(s) representation as well when they are switched on after that. Hence, we remember last
1597        * used soft wraps mode and perform update if we see that the current value differs from the stored.
1598        */
1599       private boolean myLastIsSelected;
1600
1601       @Override
1602       protected Editor getEditor(AnActionEvent e) {
1603         return myEditor;
1604       }
1605
1606       @Override
1607       public boolean isSelected(AnActionEvent e) {
1608         boolean result = super.isSelected(e);
1609         if (result ^ myLastIsSelected) {
1610           setSelected(null, result);
1611         }
1612         return myLastIsSelected = result;
1613       }
1614
1615       @Override
1616       public void setSelected(AnActionEvent e, final boolean state) {
1617         super.setSelected(e, state);
1618         if (myEditor == null) {
1619           return;
1620         }
1621
1622         final String placeholder = myCommandLineFolding.getPlaceholder(0);
1623         final FoldingModel foldingModel = myEditor.getFoldingModel();
1624         final int firstLineEnd = myEditor.getDocument().getLineEndOffset(0);
1625         foldingModel.runBatchFoldingOperation(new Runnable() {
1626           @Override
1627           public void run() {
1628             FoldRegion[] regions = foldingModel.getAllFoldRegions();
1629             if (regions.length > 0 && regions[0].getStartOffset() == 0 && regions[0].getEndOffset() == firstLineEnd) {
1630               foldingModel.removeFoldRegion(regions[0]);
1631             }
1632             if (placeholder != null) {
1633               FoldRegion foldRegion = foldingModel.addFoldRegion(0, firstLineEnd, placeholder);
1634               if (foldRegion != null) {
1635                 foldRegion.setExpanded(false);
1636               }
1637             }
1638           }
1639         });
1640       }
1641     };
1642     final AnAction autoScrollToTheEndAction = new ScrollToTheEndToolbarAction(myEditor);
1643
1644     //Initializing custom actions
1645     final AnAction[] consoleActions = new AnAction[6 + customActions.size()];
1646     consoleActions[0] = prevAction;
1647     consoleActions[1] = nextAction;
1648     consoleActions[2] = switchSoftWrapsAction;
1649     consoleActions[3] = autoScrollToTheEndAction;
1650     consoleActions[4] = ActionManager.getInstance().getAction("Print");
1651     consoleActions[5] = new ClearAllAction(this);
1652     for (int i = 0; i < customActions.size(); ++i) {
1653       consoleActions[i + 6] = customActions.get(i);
1654     }
1655     ConsoleActionsPostProcessor[] postProcessors = Extensions.getExtensions(ConsoleActionsPostProcessor.EP_NAME);
1656     AnAction[] result = consoleActions;
1657     for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
1658       result = postProcessor.postProcess(this, result);
1659     }
1660     return result;
1661   }
1662
1663   @Override
1664   public void allowHeavyFilters() {
1665     myAllowHeavyFilters = true;
1666   }
1667
1668   @Override
1669   public void addChangeListener(@NotNull final ChangeListener listener, @NotNull final Disposable parent) {
1670     myListeners.add(listener);
1671     Disposer.register(parent, new Disposable() {
1672       @Override
1673       public void dispose() {
1674         myListeners.remove(listener);
1675       }
1676     });
1677   }
1678
1679   /**
1680    * insert text to document
1681    *
1682    * @param s      inserted text
1683    * @param offset relatively to all document text
1684    */
1685   private void insertUserText(final String s, int offset) {
1686     ApplicationManager.getApplication().assertIsDispatchThread();
1687     final ConsoleViewImpl consoleView = this;
1688     final ConsoleBuffer buffer = consoleView.myBuffer;
1689     final Editor editor = consoleView.myEditor;
1690     final Document document = editor.getDocument();
1691     final int startOffset;
1692
1693     String textToUse = StringUtil.convertLineSeparators(s);
1694     synchronized (consoleView.LOCK) {
1695       if (consoleView.myTokens.isEmpty()) {
1696         addToken(0, null, ConsoleViewContentType.SYSTEM_OUTPUT);
1697       }
1698       final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1699       if (info.contentType != ConsoleViewContentType.USER_INPUT && !StringUtil.containsChar(textToUse, '\n')) {
1700         consoleView.print(textToUse, ConsoleViewContentType.USER_INPUT);
1701         consoleView.flushDeferredText();
1702         editor.getCaretModel().moveToOffset(document.getTextLength());
1703         editor.getSelectionModel().removeSelection();
1704         return;
1705       }
1706       if (info.contentType != ConsoleViewContentType.USER_INPUT) {
1707         insertUserText("temp", offset);
1708         final TokenInfo newInfo = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1709         replaceUserText(textToUse, newInfo.startOffset, newInfo.endOffset);
1710         return;
1711       }
1712
1713       final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
1714       if (offset > info.endOffset) {
1715         startOffset = info.endOffset;
1716       }
1717       else {
1718         startOffset = Math.max(deferredOffset, Math.max(info.startOffset, offset));
1719       }
1720
1721       buffer.addUserText(startOffset - deferredOffset, textToUse);
1722
1723       int charCountToAdd = textToUse.length();
1724       info.endOffset += charCountToAdd;
1725       consoleView.myContentSize += charCountToAdd;
1726     }
1727
1728     try {
1729       myInDocumentUpdate = true;
1730       document.insertString(startOffset, textToUse);
1731     }
1732     finally {
1733       myInDocumentUpdate = false;
1734     }
1735     // Math.max is needed when cyclic buffer is used
1736     editor.getCaretModel().moveToOffset(Math.min(startOffset + textToUse.length(), document.getTextLength()));
1737     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1738   }
1739
1740   /**
1741    * replace text
1742    *
1743    * @param s     text for replace
1744    * @param start relatively to all document text
1745    * @param end   relatively to all document text
1746    */
1747   private void replaceUserText(final String s, int start, int end) {
1748     if (start == end) {
1749       insertUserText(s, start);
1750       return;
1751     }
1752     final ConsoleViewImpl consoleView = this;
1753     final ConsoleBuffer buffer = consoleView.myBuffer;
1754     final Editor editor = consoleView.myEditor;
1755     final Document document = editor.getDocument();
1756     final int startOffset;
1757     final int endOffset;
1758
1759     synchronized (consoleView.LOCK) {
1760       if (consoleView.myTokens.isEmpty()) return;
1761       final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1762       if (info.contentType != ConsoleViewContentType.USER_INPUT) {
1763         consoleView.print(s, ConsoleViewContentType.USER_INPUT);
1764         consoleView.flushDeferredText();
1765         editor.getCaretModel().moveToOffset(document.getTextLength());
1766         editor.getSelectionModel().removeSelection();
1767         return;
1768       }
1769       if (buffer.getUserInputLength() <= 0) return;
1770
1771       final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
1772
1773       startOffset = getStartOffset(start, info, deferredOffset);
1774       endOffset = getEndOffset(end, info);
1775
1776       if (startOffset == -1 ||
1777           endOffset == -1 ||
1778           endOffset <= startOffset) {
1779         editor.getSelectionModel().removeSelection();
1780         editor.getCaretModel().moveToOffset(start);
1781         return;
1782       }
1783       int charCountToReplace = s.length() - endOffset + startOffset;
1784
1785       buffer.replaceUserText(startOffset - deferredOffset, endOffset - deferredOffset, s);
1786
1787       info.endOffset += charCountToReplace;
1788       if (info.startOffset == info.endOffset) {
1789         consoleView.myTokens.remove(consoleView.myTokens.size() - 1);
1790       }
1791       consoleView.myContentSize += charCountToReplace;
1792     }
1793
1794     try {
1795       myInDocumentUpdate = true;
1796       document.replaceString(startOffset, endOffset, s);
1797     }
1798     finally {
1799       myInDocumentUpdate = false;
1800     }
1801     editor.getCaretModel().moveToOffset(startOffset + s.length());
1802     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1803     editor.getSelectionModel().removeSelection();
1804   }
1805
1806   /**
1807    * delete text
1808    *
1809    * @param offset relatively to all document text
1810    * @param length length of deleted text
1811    */
1812   private void deleteUserText(int offset, int length) {
1813     ConsoleViewImpl consoleView = this;
1814     ConsoleBuffer buffer = consoleView.myBuffer;
1815     final Editor editor = consoleView.myEditor;
1816     final Document document = editor.getDocument();
1817     final int startOffset;
1818     final int endOffset;
1819
1820     synchronized (consoleView.LOCK) {
1821       if (consoleView.myTokens.isEmpty()) return;
1822       final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
1823       if (info.contentType != ConsoleViewContentType.USER_INPUT) return;
1824       if (myBuffer.getUserInputLength() == 0) return;
1825
1826       final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
1827       startOffset = getStartOffset(offset, info, deferredOffset);
1828       endOffset = getEndOffset(offset + length, info);
1829       if (startOffset == -1 ||
1830           endOffset == -1 ||
1831           endOffset <= startOffset ||
1832           startOffset < deferredOffset) {
1833         editor.getSelectionModel().removeSelection();
1834         editor.getCaretModel().moveToOffset(offset);
1835         return;
1836       }
1837
1838       buffer.removeUserText(startOffset - deferredOffset, endOffset - deferredOffset);
1839     }
1840
1841     try {
1842       myInDocumentUpdate = true;
1843       document.deleteString(startOffset, endOffset);
1844     }
1845     finally {
1846       myInDocumentUpdate = false;
1847     }
1848     editor.getCaretModel().moveToOffset(startOffset);
1849     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1850     editor.getSelectionModel().removeSelection();
1851   }
1852
1853   //util methods for add, replace, delete methods
1854   private static int getStartOffset(int offset, TokenInfo info, int deferredOffset) {
1855     int startOffset;
1856     if (offset >= info.startOffset && offset < info.endOffset) {
1857       startOffset = Math.max(offset, deferredOffset);
1858     }
1859     else if (offset < info.startOffset) {
1860       startOffset = Math.max(info.startOffset, deferredOffset);
1861     }
1862     else {
1863       startOffset = -1;
1864     }
1865     return startOffset;
1866   }
1867
1868   private static int getEndOffset(int offset, TokenInfo info) {
1869     int endOffset;
1870     if (offset > info.endOffset) {
1871       endOffset = info.endOffset;
1872     }
1873     else if (offset <= info.startOffset) {
1874       endOffset = -1;
1875     }
1876     else {
1877       endOffset = offset;
1878     }
1879     return endOffset;
1880   }
1881
1882   public boolean isRunning() {
1883     return myState.isRunning();
1884   }
1885
1886   /**
1887    * Command line used to launch application/test from idea may be quite long.
1888    * Hence, it takes many visual lines during representation if soft wraps are enabled
1889    * or, otherwise, takes many columns and makes horizontal scrollbar thumb too small.
1890    * <p/>
1891    * Our point is to fold such long command line and represent it as a single visual line by default.
1892    */
1893   private class CommandLineFolding extends ConsoleFolding {
1894
1895     /**
1896      * Checks if target line should be folded and returns its placeholder if the examination succeeds.
1897      *
1898      * @param line index of line to check
1899      * @return placeholder text if given line should be folded; <code>null</code> otherwise
1900      */
1901     @Nullable
1902     private String getPlaceholder(int line) {
1903       if (myEditor == null || line != 0) {
1904         return null;
1905       }
1906
1907       String text = EditorHyperlinkSupport.getLineText(myEditor.getDocument(), 0, false);
1908       // Don't fold the first line if the line is not that big.
1909       if (text.length() < 1000) {
1910         return null;
1911       }
1912       int index = 0;
1913       if (text.charAt(0) == '"') {
1914         index = text.indexOf('"', 1) + 1;
1915       }
1916       if (index == 0) {
1917         boolean nonWhiteSpaceFound = false;
1918         for (; index < text.length(); index++) {
1919           char c = text.charAt(index);
1920           if (c != ' ' && c != '\t') {
1921             nonWhiteSpaceFound = true;
1922             continue;
1923           }
1924           if (nonWhiteSpaceFound) {
1925             break;
1926           }
1927         }
1928       }
1929       assert index <= text.length();
1930       return text.substring(0, index) + " ...";
1931     }
1932
1933     @Override
1934     public boolean shouldFoldLine(String line) {
1935       return false;
1936     }
1937
1938     @Override
1939     public String getPlaceholderText(List<String> lines) {
1940       // Is not expected to be called.
1941       return "<...>";
1942     }
1943   }
1944
1945   private class MyFlushRunnable implements Runnable {
1946     private volatile boolean myValid = true;
1947     @Override
1948     public final void run() {
1949       synchronized (myCurrentRequests) {
1950         myCurrentRequests.remove(this);
1951       }
1952       if (myValid) {
1953         doRun();
1954       }
1955     }
1956
1957     protected void doRun() {
1958       flushDeferredText();
1959     }
1960
1961     public void invalidate() {
1962       myValid = false;
1963     }
1964
1965     public boolean isValid() {
1966       return myValid;
1967     }
1968
1969     @Override
1970     public boolean equals(Object o) {
1971       if (this == o) return true;
1972       if (o == null || getClass() != o.getClass()) return false;
1973
1974       MyFlushRunnable runnable = (MyFlushRunnable)o;
1975
1976       return myValid == runnable.myValid;
1977     }
1978
1979     @Override
1980     public int hashCode() {
1981       return getClass().hashCode();
1982     }
1983   }
1984
1985   private final class MyClearRunnable extends MyFlushRunnable {
1986     @Override
1987     public void doRun() {
1988       flushDeferredText(true);
1989     }
1990   }
1991
1992   @NotNull
1993   public Project getProject() {
1994     return myProject;
1995   }
1996
1997   private class HyperlinkNavigationAction extends DumbAwareAction {
1998     @Override
1999     public void actionPerformed(AnActionEvent e) {
2000       Runnable runnable = myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition());
2001       assert runnable != null;
2002       runnable.run();
2003     }
2004
2005     @Override
2006     public void update(AnActionEvent e) {
2007       e.getPresentation().setEnabled(myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition()) != null);
2008     }
2009   }
2010
2011   @NotNull
2012   public String getText() {
2013     return myEditor.getDocument().getText();
2014   }
2015 }
2016