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