git: enable progress output foldings for vcs console only
[idea/community.git] / platform / lang-impl / src / com / intellij / execution / impl / ConsoleViewImpl.java
1 // Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2
3 package com.intellij.execution.impl;
4
5 import com.google.common.base.CharMatcher;
6 import com.intellij.codeInsight.folding.impl.FoldingUtil;
7 import com.intellij.codeInsight.navigation.IncrementalSearchHandler;
8 import com.intellij.codeInsight.template.impl.editorActions.TypedActionHandlerBase;
9 import com.intellij.execution.ConsoleFolding;
10 import com.intellij.execution.ExecutionBundle;
11 import com.intellij.execution.actions.ClearConsoleAction;
12 import com.intellij.execution.actions.ConsoleActionsPostProcessor;
13 import com.intellij.execution.actions.EOFAction;
14 import com.intellij.execution.filters.*;
15 import com.intellij.execution.filters.Filter.ResultItem;
16 import com.intellij.execution.process.ProcessHandler;
17 import com.intellij.execution.ui.ConsoleView;
18 import com.intellij.execution.ui.ConsoleViewContentType;
19 import com.intellij.execution.ui.ObservableConsoleView;
20 import com.intellij.ide.CommonActionsManager;
21 import com.intellij.ide.OccurenceNavigator;
22 import com.intellij.ide.startup.StartupManagerEx;
23 import com.intellij.openapi.Disposable;
24 import com.intellij.openapi.actionSystem.*;
25 import com.intellij.openapi.application.ApplicationManager;
26 import com.intellij.openapi.application.ModalityState;
27 import com.intellij.openapi.application.ReadAction;
28 import com.intellij.openapi.command.undo.UndoUtil;
29 import com.intellij.openapi.diagnostic.Logger;
30 import com.intellij.openapi.editor.*;
31 import com.intellij.openapi.editor.actionSystem.EditorActionHandler;
32 import com.intellij.openapi.editor.actionSystem.EditorActionManager;
33 import com.intellij.openapi.editor.actionSystem.TypedAction;
34 import com.intellij.openapi.editor.actionSystem.TypedActionHandler;
35 import com.intellij.openapi.editor.actions.ScrollToTheEndToolbarAction;
36 import com.intellij.openapi.editor.actions.ToggleUseSoftWrapsToolbarAction;
37 import com.intellij.openapi.editor.colors.EditorColorsManager;
38 import com.intellij.openapi.editor.event.EditorMouseEvent;
39 import com.intellij.openapi.editor.ex.DocumentEx;
40 import com.intellij.openapi.editor.ex.EditorEx;
41 import com.intellij.openapi.editor.ex.MarkupModelEx;
42 import com.intellij.openapi.editor.ex.RangeHighlighterEx;
43 import com.intellij.openapi.editor.ex.util.EditorUtil;
44 import com.intellij.openapi.editor.impl.ContextMenuPopupHandler;
45 import com.intellij.openapi.editor.impl.DocumentImpl;
46 import com.intellij.openapi.editor.impl.DocumentMarkupModel;
47 import com.intellij.openapi.editor.impl.RangeMarkerImpl;
48 import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces;
49 import com.intellij.openapi.editor.markup.*;
50 import com.intellij.openapi.fileEditor.OpenFileDescriptor;
51 import com.intellij.openapi.ide.CopyPasteManager;
52 import com.intellij.openapi.keymap.Keymap;
53 import com.intellij.openapi.keymap.KeymapManager;
54 import com.intellij.openapi.keymap.KeymapUtil;
55 import com.intellij.openapi.project.*;
56 import com.intellij.openapi.util.*;
57 import com.intellij.openapi.util.text.StringUtil;
58 import com.intellij.psi.search.GlobalSearchScope;
59 import com.intellij.ui.IdeBorderFactory;
60 import com.intellij.ui.SideBorder;
61 import com.intellij.ui.awt.RelativePoint;
62 import com.intellij.util.*;
63 import com.intellij.util.text.CharArrayUtil;
64 import com.intellij.util.ui.UIUtil;
65 import org.jetbrains.annotations.*;
66
67 import javax.swing.*;
68 import java.awt.*;
69 import java.awt.datatransfer.DataFlavor;
70 import java.awt.event.MouseAdapter;
71 import java.awt.event.MouseEvent;
72 import java.awt.event.MouseWheelEvent;
73 import java.io.IOException;
74 import java.util.List;
75 import java.util.*;
76 import java.util.concurrent.*;
77 import java.util.concurrent.atomic.AtomicBoolean;
78
79 public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableConsoleView, DataProvider, OccurenceNavigator {
80   @NonNls private static final String CONSOLE_VIEW_POPUP_MENU = "ConsoleView.PopupMenu";
81   private static final Logger LOG = Logger.getInstance(ConsoleViewImpl.class);
82
83   private static final int DEFAULT_FLUSH_DELAY = SystemProperties.getIntProperty("console.flush.delay.ms", 200);
84
85   public static final Key<ConsoleViewImpl> CONSOLE_VIEW_IN_EDITOR_VIEW = Key.create("CONSOLE_VIEW_IN_EDITOR_VIEW");
86   private static final Key<ConsoleViewContentType> CONTENT_TYPE = Key.create("ConsoleViewContentType");
87   private static final Key<Boolean> USER_INPUT_SENT = Key.create("USER_INPUT_SENT");
88   private static final Key<Boolean> MANUAL_HYPERLINK = Key.create("MANUAL_HYPERLINK");
89   private static final char BACKSPACE = '\b';
90
91   private static boolean ourTypedHandlerInitialized;
92   private final Alarm myFlushUserInputAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
93   private static final CharMatcher NEW_LINE_MATCHER = CharMatcher.anyOf("\n\r");
94
95   private final CommandLineFolding myCommandLineFolding = new CommandLineFolding();
96
97   private final DisposedPsiManagerCheck myPsiDisposedCheck;
98
99   private final boolean myIsViewer;
100   @NotNull
101   private ConsoleState myState;
102
103   private final Alarm mySpareTimeAlarm = new Alarm(this);
104
105   @NotNull
106   private final Alarm myHeavyAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
107   private volatile int myHeavyUpdateTicket;
108   private final Collection<ChangeListener> myListeners = new CopyOnWriteArraySet<>();
109
110   private final List<AnAction> customActions = new ArrayList<>();
111   /** the text from {@link #print(String, ConsoleViewContentType)} goes there and stays there until {@link #flushDeferredText()} is called */
112   private final TokenBuffer myDeferredBuffer = new TokenBuffer(ConsoleBuffer.useCycleBuffer() ? ConsoleBuffer.getCycleBufferSize() : Integer.MAX_VALUE);
113
114   private boolean myUpdateFoldingsEnabled = true;
115
116   private EditorHyperlinkSupport myHyperlinks;
117   private MyDiffContainer myJLayeredPane;
118   private JPanel myMainPanel;
119   private boolean myAllowHeavyFilters;
120   private boolean myLastStickingToEnd;
121   private boolean myCancelStickToEnd;
122
123   private final Alarm myFlushAlarm = new Alarm(this);
124
125   private final Project myProject;
126
127   private boolean myOutputPaused;
128
129   private EditorEx myEditor;
130
131   private final Object LOCK = new Object();
132
133   private String myHelpId;
134
135   private final boolean myUsePredefinedMessageFilter;
136
137   private final GlobalSearchScope mySearchScope;
138
139   private final List<Filter> myCustomFilters = new SmartList<>();
140
141   @NotNull
142   private final InputFilter myInputMessageFilter;
143
144   public ConsoleViewImpl(@NotNull Project project, boolean viewer) {
145     this(project, GlobalSearchScope.allScope(project), viewer, true);
146   }
147
148   public ConsoleViewImpl(@NotNull Project project,
149                          @NotNull GlobalSearchScope searchScope,
150                          boolean viewer,
151                          boolean usePredefinedMessageFilter) {
152     this(project, searchScope, viewer,
153          new ConsoleState.NotStartedStated() {
154            @NotNull
155            @Override
156            public ConsoleState attachTo(@NotNull ConsoleViewImpl console, @NotNull ProcessHandler processHandler) {
157              return new ConsoleViewRunningState(console, processHandler, this, true, true);
158            }
159          },
160          usePredefinedMessageFilter);
161   }
162
163   protected ConsoleViewImpl(@NotNull Project project,
164                             @NotNull GlobalSearchScope searchScope,
165                             boolean viewer,
166                             @NotNull ConsoleState initialState,
167                             boolean usePredefinedMessageFilter) {
168     super(new BorderLayout());
169     initTypedHandler();
170     myIsViewer = viewer;
171     myState = initialState;
172     myPsiDisposedCheck = new DisposedPsiManagerCheck(project);
173     myProject = project;
174     myUsePredefinedMessageFilter = usePredefinedMessageFilter;
175     mySearchScope = searchScope;
176
177     List<ConsoleInputFilterProvider> inputFilters = ConsoleInputFilterProvider.INPUT_FILTER_PROVIDERS.getExtensionList();
178     if (!inputFilters.isEmpty()) {
179       CompositeInputFilter compositeInputFilter = new CompositeInputFilter(project);
180       myInputMessageFilter = compositeInputFilter;
181       for (ConsoleInputFilterProvider eachProvider : inputFilters) {
182         InputFilter[] filters = eachProvider.getDefaultFilters(project);
183         for (InputFilter filter : filters) {
184           compositeInputFilter.addFilter(filter);
185         }
186       }
187     }
188     else {
189       myInputMessageFilter = (text, contentType) -> null;
190     }
191
192     project.getMessageBus().connect(this).subscribe(DumbService.DUMB_MODE, new DumbService.DumbModeListener() {
193       private long myLastStamp;
194
195       @Override
196       public void enteredDumbMode() {
197         if (myEditor == null) return;
198         myLastStamp = myEditor.getDocument().getModificationStamp();
199
200       }
201
202       @Override
203       public void exitDumbMode() {
204         ApplicationManager.getApplication().invokeLater(() -> {
205           if (myEditor == null || project.isDisposed() || DumbService.getInstance(project).isDumb()) return;
206
207           DocumentEx document = myEditor.getDocument();
208           if (myLastStamp != document.getModificationStamp()) {
209             rehighlightHyperlinksAndFoldings();
210           }
211         });
212       }
213     });
214     ApplicationManager.getApplication().getMessageBus().connect(this).subscribe(EditorColorsManager.TOPIC, scheme -> {
215       ApplicationManager.getApplication().assertIsDispatchThread();
216       if (isDisposed() || myEditor == null) return;
217       MarkupModel model = DocumentMarkupModel.forDocument(myEditor.getDocument(), project, false);
218       for (RangeHighlighter tokenMarker : model.getAllHighlighters()) {
219         ConsoleViewContentType contentType = tokenMarker.getUserData(CONTENT_TYPE);
220         if (contentType != null && contentType.getAttributesKey() == null && tokenMarker instanceof RangeHighlighterEx) {
221           ((RangeHighlighterEx)tokenMarker).setTextAttributes(contentType.getAttributes());
222         }
223       }
224     });
225   }
226
227   private static synchronized void initTypedHandler() {
228     if (ourTypedHandlerInitialized) return;
229     EditorActionManager.getInstance();
230     TypedAction typedAction = TypedAction.getInstance();
231     typedAction.setupHandler(new MyTypedHandler(typedAction.getHandler()));
232     ourTypedHandlerInitialized = true;
233   }
234
235   public Editor getEditor() {
236     return myEditor;
237   }
238
239   public EditorHyperlinkSupport getHyperlinks() {
240     return myHyperlinks;
241   }
242
243   public void scrollToEnd() {
244     if (myEditor == null) return;
245     EditorUtil.scrollToTheEnd(myEditor, true);
246     myCancelStickToEnd = false;
247   }
248
249   public void foldImmediately() {
250     ApplicationManager.getApplication().assertIsDispatchThread();
251     if (!myFlushAlarm.isEmpty()) {
252       cancelAllFlushRequests();
253       flushDeferredText();
254     }
255
256     FoldingModel model = myEditor.getFoldingModel();
257     model.runBatchFoldingOperation(() -> {
258       for (FoldRegion region : model.getAllFoldRegions()) {
259         model.removeFoldRegion(region);
260       }
261     });
262
263     updateFoldings(0, myEditor.getDocument().getLineCount() - 1);
264   }
265
266   @Override
267   public void attachToProcess(@NotNull ProcessHandler processHandler) {
268     myState = myState.attachTo(this, processHandler);
269   }
270
271   @Override
272   public void clear() {
273     if (myEditor == null) return;
274     synchronized (LOCK) {
275       // real document content will be cleared on next flush;
276       myDeferredBuffer.clear();
277     }
278     if (!myFlushAlarm.isDisposed()) {
279       cancelAllFlushRequests();
280       addFlushRequest(0, CLEAR);
281       cancelHeavyAlarm();
282     }
283   }
284
285   @Override
286   public void scrollTo(int offset) {
287     if (myEditor == null) return;
288     final class ScrollRunnable extends FlushRunnable {
289       private ScrollRunnable() {
290         super(true); // each request must be executed
291       }
292
293       @Override
294       public void doRun() {
295         flushDeferredText();
296         if (myEditor == null) return;
297         int moveOffset = Math.min(offset, myEditor.getDocument().getTextLength());
298         if (ConsoleBuffer.useCycleBuffer() && moveOffset >= myEditor.getDocument().getTextLength()) {
299           moveOffset = 0;
300         }
301         myEditor.getCaretModel().moveToOffset(moveOffset);
302         myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
303       }
304     }
305     addFlushRequest(0, new ScrollRunnable());
306   }
307
308   @Override
309   public void requestScrollingToEnd() {
310     if (myEditor == null) {
311       return;
312     }
313
314     addFlushRequest(0, new FlushRunnable(true) {
315       @Override
316       public void doRun() {
317         flushDeferredText();
318         if (myEditor != null && !myFlushAlarm.isDisposed()) {
319           scrollToEnd();
320         }
321       }
322     });
323   }
324
325   private void addFlushRequest(int millis, @NotNull FlushRunnable flushRunnable) {
326     flushRunnable.queue(millis);
327   }
328
329   @Override
330   public void setOutputPaused(boolean value) {
331     myOutputPaused = value;
332     if (!value) {
333       requestFlushImmediately();
334     }
335   }
336
337   @Override
338   public boolean isOutputPaused() {
339     return myOutputPaused;
340   }
341
342   private boolean keepSlashR = true;
343   public void setEmulateCarriageReturn(boolean emulate) {
344     keepSlashR = emulate;
345   }
346
347   @Override
348   public boolean hasDeferredOutput() {
349     synchronized (LOCK) {
350       return myDeferredBuffer.length() > 0;
351     }
352   }
353
354   @Override
355   public void performWhenNoDeferredOutput(@NotNull Runnable runnable) {
356     //Q: implement in another way without timer?
357     if (hasDeferredOutput()) {
358       performLaterWhenNoDeferredOutput(runnable);
359     }
360     else {
361       runnable.run();
362     }
363   }
364
365   private void performLaterWhenNoDeferredOutput(@NotNull Runnable runnable) {
366     if (mySpareTimeAlarm.isDisposed()) return;
367     if (myJLayeredPane == null) {
368       getComponent();
369     }
370     mySpareTimeAlarm.addRequest(
371       () -> performWhenNoDeferredOutput(runnable),
372       100,
373       ModalityState.stateForComponent(myJLayeredPane)
374     );
375   }
376
377   @Override
378   @NotNull
379   public JComponent getComponent() {
380     if (myMainPanel == null) {
381       myMainPanel = new JPanel(new BorderLayout());
382       myJLayeredPane = new MyDiffContainer(myMainPanel, createCompositeFilter().getUpdateMessage());
383       Disposer.register(this, myJLayeredPane);
384       add(myJLayeredPane, BorderLayout.CENTER);
385     }
386
387     if (myEditor == null) {
388       initConsoleEditor();
389       requestFlushImmediately();
390       myMainPanel.add(createCenterComponent(), BorderLayout.CENTER);
391     }
392     return this;
393   }
394
395   @NotNull
396   protected CompositeFilter createCompositeFilter() {
397     List<Filter> predefinedFilters = myUsePredefinedMessageFilter ?
398                                        ConsoleViewUtil.computeConsoleFilters(myProject, this, mySearchScope) :
399                                        Collections.emptyList();
400     CompositeFilter compositeFilter = new CompositeFilter(myProject, predefinedFilters);
401     compositeFilter.setForceUseAllFilters(true);
402     myCustomFilters.forEach(compositeFilter::addFilter);
403     return compositeFilter;
404   }
405
406   /**
407    * Adds transparent (actually, non-opaque) component over console.
408    * It will be as big as console. Use it to draw on console because it does not prevent user from console usage.
409    *
410    * @param component component to add
411    */
412   public final void addLayerToPane(@NotNull JComponent component) {
413     getComponent(); // Make sure component exists
414     component.setOpaque(false);
415     component.setVisible(true);
416     myJLayeredPane.add(component, 0);
417   }
418
419   private void initConsoleEditor() {
420     myEditor = createConsoleEditor();
421     registerConsoleEditorActions();
422     myEditor.getScrollPane().setBorder(IdeBorderFactory.createBorder(SideBorder.LEFT));
423     MouseAdapter mouseListener = new MouseAdapter() {
424       @Override
425       public void mousePressed(MouseEvent e) {
426         updateStickToEndState(true);
427       }
428
429       @Override
430       public void mouseDragged(MouseEvent e) {
431         updateStickToEndState(false);
432       }
433
434       @Override
435       public void mouseWheelMoved(MouseWheelEvent e) {
436         if (e.isShiftDown()) return; // ignore horizontal scrolling
437         updateStickToEndState(false);
438       }
439     };
440     myEditor.getScrollPane().addMouseWheelListener(mouseListener);
441     myEditor.getScrollPane().getVerticalScrollBar().addMouseListener(mouseListener);
442     myEditor.getScrollPane().getVerticalScrollBar().addMouseMotionListener(mouseListener);
443     myHyperlinks = EditorHyperlinkSupport.get(myEditor);
444     myEditor.getScrollingModel().addVisibleAreaListener(e -> {
445       // There is a possible case that the console text is populated while the console is not shown (e.g. we're debugging and
446       // 'Debugger' tab is active while 'Console' is not). It's also possible that newly added text contains long lines that
447       // are soft wrapped. We want to update viewport position then when the console becomes visible.
448       Rectangle oldR = e.getOldRectangle();
449
450       if (oldR != null && oldR.height <= 0 &&
451           e.getNewRectangle().height > 0 &&
452           isStickingToEnd()) {
453         scrollToEnd();
454       }
455     });
456   }
457
458   private void updateStickToEndState(boolean useImmediatePosition) {
459     if (myEditor == null) return;
460
461     JScrollBar scrollBar = myEditor.getScrollPane().getVerticalScrollBar();
462     int scrollBarPosition = useImmediatePosition ? scrollBar.getValue() :
463                             myEditor.getScrollingModel().getVisibleAreaOnScrollingFinished().y;
464     boolean vscrollAtBottom = scrollBarPosition == scrollBar.getMaximum() - scrollBar.getVisibleAmount();
465     boolean stickingToEnd = isStickingToEnd();
466
467     if (!vscrollAtBottom && stickingToEnd) {
468       myCancelStickToEnd = true;
469     } else if (vscrollAtBottom && !stickingToEnd) {
470       scrollToEnd();
471     }
472   }
473
474   @NotNull
475   protected JComponent createCenterComponent() {
476     return myEditor.getComponent();
477   }
478
479   @Override
480   public void dispose() {
481     myState = myState.dispose();
482     if (myEditor != null) {
483       cancelAllFlushRequests();
484       mySpareTimeAlarm.cancelAllRequests();
485       disposeEditor();
486       myEditor.putUserData(CONSOLE_VIEW_IN_EDITOR_VIEW, null);
487       synchronized (LOCK) {
488         myDeferredBuffer.clear();
489       }
490       myEditor = null;
491       myHyperlinks = null;
492     }
493   }
494
495   private void cancelAllFlushRequests() {
496     myFlushAlarm.cancelAllRequests();
497     CLEAR.clearRequested();
498     FLUSH.clearRequested();
499   }
500
501   @TestOnly
502   public void waitAllRequests() {
503     ApplicationManager.getApplication().assertIsDispatchThread();
504     Future<?> future = ApplicationManager.getApplication().executeOnPooledThread(() -> {
505       while (true) {
506         try {
507           myFlushAlarm.waitForAllExecuted(10, TimeUnit.SECONDS);
508           myFlushUserInputAlarm.waitForAllExecuted(10, TimeUnit.SECONDS);
509           myFlushAlarm.waitForAllExecuted(10, TimeUnit.SECONDS);
510           myFlushUserInputAlarm.waitForAllExecuted(10, TimeUnit.SECONDS);
511           return;
512         }
513         catch (CancellationException e) {
514           //try again
515         }
516         catch (InterruptedException | ExecutionException | TimeoutException e) {
517           throw new RuntimeException(e);
518         }
519       }
520     });
521     try {
522       while (true) {
523         try {
524           future.get(10, TimeUnit.MILLISECONDS);
525           break;
526         }
527         catch (TimeoutException ignored) {
528         }
529         UIUtil.dispatchAllInvocationEvents();
530       }
531     }
532     catch (InterruptedException | ExecutionException e) {
533       throw new RuntimeException(e);
534     }
535   }
536
537   protected void disposeEditor() {
538     UIUtil.invokeAndWaitIfNeeded((Runnable)() -> {
539       if (!myEditor.isDisposed()) {
540         EditorFactory.getInstance().releaseEditor(myEditor);
541       }
542     });
543   }
544
545   @Override
546   public void print(@NotNull String text, @NotNull ConsoleViewContentType contentType) {
547     List<Pair<String, ConsoleViewContentType>> result = myInputMessageFilter.applyFilter(text, contentType);
548     if (result == null) {
549       print(text, contentType, null);
550     }
551     else {
552       for (Pair<String, ConsoleViewContentType> pair : result) {
553         if (pair.first != null) {
554           print(pair.first, pair.second == null ? contentType : pair.second, null);
555         }
556       }
557     }
558   }
559
560   protected void print(@NotNull String text, @NotNull ConsoleViewContentType contentType, @Nullable HyperlinkInfo info) {
561     text = StringUtil.convertLineSeparators(text, keepSlashR);
562     synchronized (LOCK) {
563       myDeferredBuffer.print(text, contentType, info);
564
565       if (contentType == ConsoleViewContentType.USER_INPUT) {
566         requestFlushImmediately();
567       }
568       else if (myEditor != null) {
569         boolean shouldFlushNow = myDeferredBuffer.length() >= myDeferredBuffer.getCycleBufferSize();
570         addFlushRequest(shouldFlushNow ? 0 : DEFAULT_FLUSH_DELAY, FLUSH);
571       }
572     }
573   }
574
575   // send text which was typed in the console to the running process
576   private void sendUserInput(@NotNull CharSequence typedText) {
577     ApplicationManager.getApplication().assertIsDispatchThread();
578     if (myState.isRunning() && NEW_LINE_MATCHER.indexIn(typedText) >= 0) {
579       StringBuilder textToSend = new StringBuilder();
580       // compute text input from the console contents:
581       // all range markers beginning from the caret offset backwards, marked as user input and not marked as already sent
582       for (RangeMarker marker = findTokenMarker(myEditor.getCaretModel().getOffset()-1);
583            marker != null;
584            marker = ((RangeMarkerImpl)marker).findRangeMarkerBefore()) {
585         ConsoleViewContentType tokenType = getTokenType(marker);
586         if (tokenType != null) {
587           if (tokenType != ConsoleViewContentType.USER_INPUT || marker.getUserData(USER_INPUT_SENT) == Boolean.TRUE) {
588             break;
589           }
590           marker.putUserData(USER_INPUT_SENT, true);
591           textToSend.insert(0, marker.getDocument().getText(TextRange.create(marker)));
592         }
593       }
594       if (textToSend.length() != 0) {
595         myFlushUserInputAlarm.addRequest(() -> {
596           if (myState.isRunning()) {
597             try {
598               // this may block forever, see IDEA-54340
599               myState.sendUserInput(textToSend.toString());
600             }
601             catch (IOException ignored) {
602             }
603           }
604         }, 0);
605       }
606     }
607   }
608
609   protected ModalityState getStateForUpdate() {
610     return null;
611   }
612
613   private void requestFlushImmediately() {
614     if (myEditor != null) {
615       addFlushRequest(0, FLUSH);
616     }
617   }
618
619   /**
620    * Holds number of symbols managed by the current console.
621    * <p/>
622    * Total number is assembled as a sum of symbols that are already pushed to the document and number of deferred symbols that
623    * are awaiting to be pushed to the document.
624    */
625   @Override
626   public int getContentSize() {
627     synchronized (LOCK) {
628       return (myEditor == null ? 0 : myEditor.getDocument().getTextLength())
629              + myDeferredBuffer.length();
630     }
631   }
632
633   @Override
634   public boolean canPause() {
635     return true;
636   }
637
638   public void flushDeferredText() {
639     ApplicationManager.getApplication().assertIsDispatchThread();
640     if (isDisposed()) return;
641     boolean shouldStickToEnd = !myCancelStickToEnd && isStickingToEnd();
642     myCancelStickToEnd = false; // Cancel only needs to last for one update. Next time, isStickingToEnd() will be false.
643
644     Ref<CharSequence> addedTextRef = Ref.create();
645     List<TokenBuffer.TokenInfo> deferredTokens;
646     Document document = myEditor.getDocument();
647
648     synchronized (LOCK) {
649       if (myOutputPaused) return;
650
651       deferredTokens = myDeferredBuffer.drain();
652       if (deferredTokens.isEmpty()) return;
653       cancelHeavyAlarm();
654     }
655
656     RangeMarker lastProcessedOutput = document.createRangeMarker(document.getTextLength(), document.getTextLength());
657
658     if (!shouldStickToEnd) {
659       myEditor.getScrollingModel().accumulateViewportChanges();
660     }
661     Collection<ConsoleViewContentType> contentTypes = new HashSet<>();
662     List<Pair<String, ConsoleViewContentType>> contents = new ArrayList<>();
663     try {
664       // the text can contain one "\r" at the start meaning we should delete the last line
665       boolean startsWithCR = deferredTokens.get(0) == TokenBuffer.CR_TOKEN;
666       if (startsWithCR) {
667         // remove last line if any
668         if (document.getLineCount() != 0) {
669           int lineStartOffset = document.getLineStartOffset(document.getLineCount() - 1);
670           document.deleteString(lineStartOffset, document.getTextLength());
671         }
672       }
673       int startIndex = startsWithCR ? 1 : 0;
674       List<TokenBuffer.TokenInfo> refinedTokens = new ArrayList<>(deferredTokens.size() - startIndex);
675       int backspacePrefixLength = evaluateBackspacesInTokens(deferredTokens, startIndex, refinedTokens);
676       if (backspacePrefixLength > 0) {
677         int lineCount = document.getLineCount();
678         if (lineCount != 0) {
679           int lineStartOffset = document.getLineStartOffset(lineCount - 1);
680           document.deleteString(Math.max(lineStartOffset, document.getTextLength() - backspacePrefixLength), document.getTextLength());
681         }
682       }
683       addedTextRef.set(TokenBuffer.getRawText(refinedTokens));
684       document.insertString(document.getTextLength(), addedTextRef.get());
685       // add token information as range markers
686       // start from the end because portion of the text can be stripped from the document beginning because of a cycle buffer
687       int offset = document.getTextLength();
688       int tokenLength = 0;
689       for (int i = refinedTokens.size() - 1; i >= 0; i--) {
690         TokenBuffer.TokenInfo token = refinedTokens.get(i);
691         contentTypes.add(token.contentType);
692         contents.add(new Pair<>(token.getText(), token.contentType));
693         tokenLength += token.length();
694         TokenBuffer.TokenInfo prevToken = i == 0 ? null : refinedTokens.get(i - 1);
695         if (prevToken != null && token.contentType == prevToken.contentType && token.getHyperlinkInfo() == prevToken.getHyperlinkInfo()) {
696           // do not create highlighter yet because can merge previous token with the current
697           continue;
698         }
699         int start = Math.max(0, offset - tokenLength);
700         if (start == offset) {
701           continue;
702         }
703         HyperlinkInfo info = token.getHyperlinkInfo();
704         if (info != null) {
705           myHyperlinks.createHyperlink(start, offset, null, info).putUserData(MANUAL_HYPERLINK, true);
706         }
707         createTokenRangeHighlighter(token.contentType, start, offset);
708         offset = start;
709         tokenLength = 0;
710       }
711     }
712     finally {
713       if (!shouldStickToEnd) {
714         myEditor.getScrollingModel().flushViewportChanges();
715       }
716     }
717     if (!contentTypes.isEmpty()) {
718       for (ChangeListener each : myListeners) {
719         each.contentAdded(contentTypes);
720       }
721     }
722     if (!contents.isEmpty()) {
723       for (ChangeListener each : myListeners) {
724         for (int i = contents.size() - 1; i >= 0; i--) {
725           each.textAdded(contents.get(i).first, contents.get(i).second);
726         }
727       }
728     }
729     myPsiDisposedCheck.performCheck();
730
731     int startLine = lastProcessedOutput.isValid() ? myEditor.getDocument().getLineNumber(lastProcessedOutput.getEndOffset()) : 0;
732     lastProcessedOutput.dispose();
733     highlightHyperlinksAndFoldings(startLine);
734
735     if (shouldStickToEnd) {
736       scrollToEnd();
737     }
738     sendUserInput(addedTextRef.get());
739   }
740
741   private static int evaluateBackspacesInTokens(@NotNull List<? extends TokenBuffer.TokenInfo> source,
742                                                 int sourceStartIndex,
743                                                 @NotNull List<? super TokenBuffer.TokenInfo> dest) {
744     int backspacesFromNextToken = 0;
745     for (int i = source.size() - 1; i >= sourceStartIndex; i--) {
746       TokenBuffer.TokenInfo token = source.get(i);
747       TokenBuffer.TokenInfo newToken;
748       if (StringUtil.containsChar(token.getText(), BACKSPACE) || backspacesFromNextToken > 0) {
749         StringBuilder tokenTextBuilder = new StringBuilder(token.getText().length() + backspacesFromNextToken);
750         tokenTextBuilder.append(token.getText());
751         for (int j = 0; j < backspacesFromNextToken; j++) {
752           tokenTextBuilder.append(BACKSPACE);
753         }
754         normalizeBackspaceCharacters(tokenTextBuilder);
755         backspacesFromNextToken = getBackspacePrefixLength(tokenTextBuilder);
756         String newText = tokenTextBuilder.substring(backspacesFromNextToken);
757         newToken = new TokenBuffer.TokenInfo(token.contentType, newText, token.getHyperlinkInfo());
758       }
759       else {
760         newToken = token;
761       }
762       dest.add(newToken);
763     }
764     Collections.reverse(dest);
765     return backspacesFromNextToken;
766   }
767
768   private static int getBackspacePrefixLength(@NotNull CharSequence text) {
769     int prefix = 0;
770     while (prefix < text.length() && text.charAt(prefix) == BACKSPACE) {
771       prefix++;
772     }
773     return prefix;
774   }
775
776   // convert all "a\bc" sequences to "c", not crossing the line boundaries in the process
777   private static void normalizeBackspaceCharacters(@NotNull StringBuilder text) {
778     int ind = StringUtil.indexOf(text, BACKSPACE);
779     if (ind < 0) {
780       return;
781     }
782     int guardLength = 0;
783     int newLength = 0;
784     for (int i = 0; i < text.length(); i++) {
785       char ch = text.charAt(i);
786       boolean append;
787       if (ch == BACKSPACE) {
788         assert guardLength <= newLength;
789         if (guardLength == newLength) {
790           // Backspace is the first char in a new line:
791           // Keep backspace at the first line (guardLength == 0) as it might be in the middle of the actual line,
792           // handle it later (see getBackspacePrefixLength).
793           // Otherwise (for non-first lines), skip backspace as it can't be interpreted if located right after line ending.
794           append = guardLength == 0;
795         }
796         else {
797           append = text.charAt(newLength - 1) == BACKSPACE;
798           if (!append) {
799             newLength--; // interpret \b: delete prev char
800           }
801         }
802       }
803       else {
804         append = true;
805       }
806       if (append) {
807         text.setCharAt(newLength, ch);
808         newLength++;
809         if (ch == '\r' || ch == '\n') guardLength = newLength;
810       }
811     }
812     text.setLength(newLength);
813   }
814
815   private void createTokenRangeHighlighter(@NotNull ConsoleViewContentType contentType,
816                                            int startOffset,
817                                            int endOffset) {
818     ApplicationManager.getApplication().assertIsDispatchThread();
819     MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
820     int layer = HighlighterLayer.SYNTAX + 1; // make custom filters able to draw their text attributes over the default ones
821     model.addRangeHighlighterAndChangeAttributes(
822       contentType.getAttributesKey(), startOffset, endOffset, layer, HighlighterTargetArea.EXACT_RANGE, false,
823       ex -> {
824         // fallback for contentTypes which provide only attributes
825         if (ex.getTextAttributesKey() == null) {
826           ex.setTextAttributes(contentType.getAttributes());
827         }
828         ex.putUserData(CONTENT_TYPE, contentType);
829       });
830   }
831
832   private boolean isDisposed() {
833     return myProject.isDisposed() || myEditor == null;
834   }
835
836   protected void doClear() {
837     ApplicationManager.getApplication().assertIsDispatchThread();
838
839     if (isDisposed()) return;
840
841     DocumentEx document = myEditor.getDocument();
842     int documentTextLength = document.getTextLength();
843     if (documentTextLength > 0) {
844       DocumentUtil.executeInBulk(document, true, () -> document.deleteString(0, documentTextLength));
845     }
846     synchronized (LOCK) {
847       clearHyperlinkAndFoldings();
848     }
849     MarkupModel model = DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
850     model.removeAllHighlighters(); // remove all empty highlighters leftovers if any
851     myEditor.getInlayModel().getInlineElementsInRange(0, 0).forEach(Disposer::dispose); // remove inlays if any
852   }
853
854   private boolean isStickingToEnd() {
855     if (myEditor == null) return myLastStickingToEnd;
856     Document document = myEditor.getDocument();
857     int caretOffset = myEditor.getCaretModel().getOffset();
858     myLastStickingToEnd = document.getLineNumber(caretOffset) >= document.getLineCount() - 1;
859     return myLastStickingToEnd;
860   }
861
862   private void clearHyperlinkAndFoldings() {
863     for (RangeHighlighter highlighter : myEditor.getMarkupModel().getAllHighlighters()) {
864       if (highlighter.getUserData(MANUAL_HYPERLINK) == null) {
865         myEditor.getMarkupModel().removeHighlighter(highlighter);
866       }
867     }
868
869     myEditor.getFoldingModel().runBatchFoldingOperation(() -> myEditor.getFoldingModel().clearFoldRegions());
870
871     cancelHeavyAlarm();
872   }
873
874   private void cancelHeavyAlarm() {
875     if (!myHeavyAlarm.isDisposed()) {
876       myHeavyAlarm.cancelAllRequests();
877       ++myHeavyUpdateTicket;
878     }
879   }
880
881   @Override
882   public Object getData(@NotNull String dataId) {
883     if (myEditor == null) {
884       return null;
885     }
886     if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
887       int offset = myEditor.getCaretModel().getOffset();
888       HyperlinkInfo info = myHyperlinks.getHyperlinkAt(offset);
889       OpenFileDescriptor openFileDescriptor = info instanceof FileHyperlinkInfo ? ((FileHyperlinkInfo)info).getDescriptor() : null;
890       if (openFileDescriptor == null || !openFileDescriptor.getFile().isValid()) {
891         return null;
892       }
893       return openFileDescriptor;
894     }
895
896     if (CommonDataKeys.EDITOR.is(dataId)) {
897       return myEditor;
898     }
899     if (PlatformDataKeys.HELP_ID.is(dataId)) {
900       return myHelpId;
901     }
902     if (LangDataKeys.CONSOLE_VIEW.is(dataId)) {
903       return this;
904     }
905     return null;
906   }
907
908   @Override
909   public void setHelpId(@NotNull String helpId) {
910     myHelpId = helpId;
911   }
912
913   public void setUpdateFoldingsEnabled(boolean updateFoldingsEnabled) {
914     myUpdateFoldingsEnabled = updateFoldingsEnabled;
915   }
916
917   @Override
918   public void addMessageFilter(@NotNull Filter filter) {
919     myCustomFilters.add(filter);
920   }
921
922   @Override
923   public void printHyperlink(@NotNull String hyperlinkText, @Nullable HyperlinkInfo info) {
924     print(hyperlinkText, ConsoleViewContentType.NORMAL_OUTPUT, info);
925   }
926
927   @NotNull
928   private EditorEx createConsoleEditor() {
929     return ReadAction.compute(() -> {
930       EditorEx editor = doCreateConsoleEditor();
931       LOG.assertTrue(UndoUtil.isUndoDisabledFor(editor.getDocument()));
932       editor.installPopupHandler(new ContextMenuPopupHandler() {
933         @Override
934         public ActionGroup getActionGroup(@NotNull EditorMouseEvent event) {
935           return getPopupGroup(event);
936         }
937       });
938
939       int bufferSize = ConsoleBuffer.useCycleBuffer() ? ConsoleBuffer.getCycleBufferSize() : 0;
940       editor.getDocument().setCyclicBufferSize(bufferSize);
941
942       editor.putUserData(CONSOLE_VIEW_IN_EDITOR_VIEW, this);
943
944       editor.getSettings().setAllowSingleLogicalLineFolding(true); // We want to fold long soft-wrapped command lines
945
946       return editor;
947     });
948   }
949
950   @NotNull
951   protected EditorEx doCreateConsoleEditor() {
952     return ConsoleViewUtil.setupConsoleEditor(myProject, true, false);
953   }
954
955   private void registerConsoleEditorActions() {
956     Shortcut[] shortcuts = KeymapUtil.getActiveKeymapShortcuts(IdeActions.ACTION_GOTO_DECLARATION).getShortcuts();
957     CustomShortcutSet shortcutSet = new CustomShortcutSet(ArrayUtil.mergeArrays(shortcuts, CommonShortcuts.ENTER.getShortcuts()));
958     new HyperlinkNavigationAction().registerCustomShortcutSet(shortcutSet, myEditor.getContentComponent());
959
960
961     if (!myIsViewer) {
962       new EnterHandler().registerCustomShortcutSet(CommonShortcuts.ENTER, myEditor.getContentComponent());
963       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_PASTE, new PasteHandler());
964       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_BACKSPACE, new BackSpaceHandler());
965       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_DELETE, new DeleteHandler());
966       registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_TAB, new TabHandler());
967
968       registerActionHandler(myEditor, EOFAction.ACTION_ID);
969     }
970   }
971
972   private static void registerActionHandler(@NotNull Editor editor, @NotNull String actionId) {
973     AnAction action = ActionManager.getInstance().getAction(actionId);
974     action.registerCustomShortcutSet(action.getShortcutSet(), editor.getContentComponent());
975   }
976
977   private static void registerActionHandler(@NotNull Editor editor, @NotNull String actionId, @NotNull AnAction action) {
978     Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
979     Shortcut[] shortcuts = keymap.getShortcuts(actionId);
980     action.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), editor.getContentComponent());
981   }
982
983   @NotNull
984   private ActionGroup getPopupGroup(@NotNull EditorMouseEvent event) {
985     ActionManager actionManager = ActionManager.getInstance();
986     HyperlinkInfo info = myHyperlinks != null ? myHyperlinks.getHyperlinkInfoByEvent(event) : null;
987     ActionGroup group = null;
988     if (info instanceof HyperlinkWithPopupMenuInfo) {
989       group = ((HyperlinkWithPopupMenuInfo)info).getPopupMenuGroup(event.getMouseEvent());
990     }
991     if (group == null) {
992       group = (ActionGroup)actionManager.getAction(CONSOLE_VIEW_POPUP_MENU);
993     }
994     List<ConsoleActionsPostProcessor> postProcessors = ConsoleActionsPostProcessor.EP_NAME.getExtensionList();
995     AnAction[] result = group.getChildren(null);
996
997     for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
998       result = postProcessor.postProcessPopupActions(this, result);
999     }
1000     return new DefaultActionGroup(result);
1001   }
1002
1003   private void highlightHyperlinksAndFoldings(int startLine) {
1004     ApplicationManager.getApplication().assertIsDispatchThread();
1005     CompositeFilter compositeFilter = createCompositeFilter();
1006     boolean canHighlightHyperlinks = !compositeFilter.isEmpty();
1007
1008     if (!canHighlightHyperlinks && !myUpdateFoldingsEnabled) {
1009       return;
1010     }
1011     DocumentEx document = myEditor.getDocument();
1012     if (document.getTextLength() == 0) return;
1013
1014     int endLine = Math.max(0, document.getLineCount() - 1);
1015
1016     if (canHighlightHyperlinks) {
1017       myHyperlinks.highlightHyperlinks(compositeFilter, startLine, endLine);
1018     }
1019
1020     if (myAllowHeavyFilters && compositeFilter.isAnyHeavy() && compositeFilter.shouldRunHeavy()) {
1021       runHeavyFilters(compositeFilter, startLine, endLine);
1022     }
1023     if (myUpdateFoldingsEnabled) {
1024       updateFoldings(startLine, endLine);
1025     }
1026   }
1027
1028   public void rehighlightHyperlinksAndFoldings() {
1029     if (myEditor == null || myProject.isDisposed()) return;
1030
1031     clearHyperlinkAndFoldings();
1032     highlightHyperlinksAndFoldings(0);
1033   }
1034
1035   private void runHeavyFilters(@NotNull CompositeFilter compositeFilter, int line1, int endLine) {
1036     int startLine = Math.max(0, line1);
1037
1038     Document document = myEditor.getDocument();
1039     int startOffset = document.getLineStartOffset(startLine);
1040     String text = document.getText(new TextRange(startOffset, document.getLineEndOffset(endLine)));
1041     Document documentCopy = new DocumentImpl(text,true);
1042     documentCopy.setReadOnly(true);
1043
1044     myJLayeredPane.startUpdating();
1045     int currentValue = myHeavyUpdateTicket;
1046     myHeavyAlarm.addRequest(() -> {
1047       if (!compositeFilter.shouldRunHeavy()) return;
1048       try {
1049         compositeFilter.applyHeavyFilter(documentCopy, startOffset, startLine, additionalHighlight ->
1050           addFlushRequest(0, new FlushRunnable(true) {
1051             @Override
1052             public void doRun() {
1053               if (myHeavyUpdateTicket != currentValue) return;
1054               TextAttributes additionalAttributes = additionalHighlight.getTextAttributes(null);
1055               if (additionalAttributes != null) {
1056                 ResultItem item = additionalHighlight.getResultItems().get(0);
1057                 myHyperlinks.addHighlighter(item.getHighlightStartOffset(), item.getHighlightEndOffset(), additionalAttributes);
1058               }
1059               else {
1060                 myHyperlinks.highlightHyperlinks(additionalHighlight, 0);
1061               }
1062             }
1063           })
1064         );
1065       }
1066       catch (IndexNotReadyException ignore) {
1067       }
1068       finally {
1069         if (myHeavyAlarm.getActiveRequestCount() <= 1) { // only the current request
1070           UIUtil.invokeLaterIfNeeded(() -> myJLayeredPane.finishUpdating());
1071         }
1072       }
1073     }, 0);
1074   }
1075
1076   protected void updateFoldings(int startLine, int endLine) {
1077     ApplicationManager.getApplication().assertIsDispatchThread();
1078     myEditor.getFoldingModel().runBatchFoldingOperation(() -> {
1079       Document document = myEditor.getDocument();
1080
1081       FoldRegion existingRegion = null;
1082       if (startLine > 0) {
1083         int prevLineStart = document.getLineStartOffset(startLine - 1);
1084         FoldRegion[] regions = FoldingUtil.getFoldRegionsAtOffset(myEditor, prevLineStart);
1085         if (regions.length == 1) {
1086           existingRegion = regions[0];
1087         }
1088       }
1089       ConsoleFolding lastFolding = findFoldingByRegion(existingRegion);
1090       int lastStartLine = Integer.MAX_VALUE;
1091       if (lastFolding != null) {
1092         int offset = existingRegion.getStartOffset();
1093         if (offset == 0) {
1094           lastStartLine = 0;
1095         }
1096         else {
1097           lastStartLine = document.getLineNumber(offset);
1098           if (document.getLineStartOffset(lastStartLine) != offset) lastStartLine++;
1099         }
1100       }
1101
1102       for (int line = startLine; line <= endLine; line++) {
1103         /*
1104         Grep Console plugin allows to fold empty lines. We need to handle this case in a special way.
1105
1106         Multiple lines are grouped into one folding, but to know when you can create the folding,
1107         you need a line which does not belong to that folding.
1108         When a new line, or a chunk of lines is printed, #addFolding is called for that lines + for an empty string
1109         (which basically does only one thing, gets a folding displayed).
1110         We do not want to process that empty string, but also we do not want to wait for another line
1111         which will create and display the folding - we'd see an unfolded stacktrace until another text came and flushed it.
1112         So therefore the condition, the last line(empty string) should still flush, but not be processed by
1113         com.intellij.execution.ConsoleFolding.
1114          */
1115         ConsoleFolding next = line < endLine ? foldingForLine(line, document) : null;
1116         if (next != lastFolding) {
1117           if (lastFolding != null) {
1118             boolean isExpanded = false;
1119             if (line > startLine && existingRegion != null && lastStartLine < startLine) {
1120               isExpanded = existingRegion.isExpanded();
1121               myEditor.getFoldingModel().removeFoldRegion(existingRegion);
1122             }
1123             addFoldRegion(document, lastFolding, lastStartLine, line - 1, isExpanded);
1124           }
1125           lastFolding = next;
1126           lastStartLine = line;
1127           existingRegion = null;
1128         }
1129       }
1130     });
1131   }
1132
1133   private static final Key<String> USED_FOLDING_FQN_KEY = Key.create("USED_FOLDING_KEY");
1134
1135   private void addFoldRegion(@NotNull Document document, @NotNull ConsoleFolding folding, int startLine, int endLine, boolean isExpanded) {
1136     List<String> toFold = new ArrayList<>(endLine - startLine + 1);
1137     for (int i = startLine; i <= endLine; i++) {
1138       toFold.add(EditorHyperlinkSupport.getLineText(document, i, false));
1139     }
1140
1141     int oStart = document.getLineStartOffset(startLine);
1142     if (oStart > 0 && folding.shouldBeAttachedToThePreviousLine()) oStart--;
1143     int oEnd = CharArrayUtil.shiftBackward(document.getImmutableCharSequence(), document.getLineEndOffset(endLine) - 1, " \t") + 1;
1144
1145     String placeholder = folding.getPlaceholderText(getProject(), toFold);
1146     FoldRegion region = placeholder == null ? null : myEditor.getFoldingModel().addFoldRegion(oStart, oEnd, placeholder);
1147     if (region != null) {
1148       region.setExpanded(isExpanded);
1149       region.putUserData(USED_FOLDING_FQN_KEY, getFoldingFqn(folding));
1150     }
1151   }
1152
1153   @Nullable
1154   @Contract("null -> null")
1155   private ConsoleFolding findFoldingByRegion(@Nullable FoldRegion region) {
1156     String lastFoldingFqn = USED_FOLDING_FQN_KEY.get(region);
1157     if (lastFoldingFqn == null) return null;
1158     ConsoleFolding consoleFolding = ConsoleFolding.EP_NAME.getByKey(lastFoldingFqn, ConsoleViewImpl.class, ConsoleViewImpl::getFoldingFqn);
1159     return consoleFolding != null && consoleFolding.isEnabledForConsole(this) ? consoleFolding : null;
1160   }
1161
1162   @NotNull
1163   private static String getFoldingFqn(@NotNull ConsoleFolding consoleFolding) {
1164     return consoleFolding.getClass().getName();
1165   }
1166
1167   @Nullable
1168   private ConsoleFolding foldingForLine(int line, @NotNull Document document) {
1169     String lineText = EditorHyperlinkSupport.getLineText(document, line, false);
1170     if (line == 0 && myCommandLineFolding.shouldFoldLine(myProject, lineText)) {
1171       return myCommandLineFolding;
1172     }
1173
1174     for (ConsoleFolding extension : ConsoleFolding.EP_NAME.getExtensions()) {
1175       if (extension.isEnabledForConsole(this) && extension.shouldFoldLine(myProject, lineText)) {
1176         return extension;
1177       }
1178     }
1179     return null;
1180   }
1181
1182   private static class ClearThisConsoleAction extends ClearConsoleAction {
1183     private final ConsoleView myConsoleView;
1184
1185     ClearThisConsoleAction(@NotNull ConsoleView consoleView) {
1186       myConsoleView = consoleView;
1187     }
1188
1189     @Override
1190     public void update(@NotNull AnActionEvent e) {
1191       boolean enabled = myConsoleView.getContentSize() > 0;
1192       e.getPresentation().setEnabled(enabled);
1193     }
1194
1195     @Override
1196     public void actionPerformed(@NotNull AnActionEvent e) {
1197       myConsoleView.clear();
1198     }
1199   }
1200
1201   /**
1202    * @deprecated use {@link ClearConsoleAction} instead
1203    */
1204   @Deprecated
1205   @ApiStatus.ScheduledForRemoval(inVersion = "2020.1")
1206   public static class ClearAllAction extends ClearConsoleAction {
1207   }
1208
1209   // finds range marker the [offset..offset+1) belongs to
1210   private RangeMarker findTokenMarker(int offset) {
1211     RangeMarker[] marker = new RangeMarker[1];
1212     MarkupModelEx model = (MarkupModelEx)DocumentMarkupModel.forDocument(myEditor.getDocument(), getProject(), true);
1213     model.processRangeHighlightersOverlappingWith(offset, offset, m->{
1214       if (getTokenType(m) == null || m.getStartOffset() > offset || offset + 1 > m.getEndOffset()) return true;
1215       marker[0] = m;
1216       return false;
1217     });
1218
1219     return marker[0];
1220   }
1221
1222   private static ConsoleViewContentType getTokenType(@Nullable RangeMarker m) {
1223     return m == null ? null : m.getUserData(CONTENT_TYPE);
1224   }
1225
1226   private static final class MyTypedHandler extends TypedActionHandlerBase {
1227     private MyTypedHandler(TypedActionHandler originalAction) {
1228       super(originalAction);
1229     }
1230
1231     @Override
1232     public void execute(@NotNull Editor editor, char charTyped, @NotNull DataContext dataContext) {
1233       ConsoleViewImpl consoleView = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
1234       if (consoleView == null || !consoleView.myState.isRunning() || consoleView.myIsViewer) {
1235         if (myOriginalHandler != null) {
1236           myOriginalHandler.execute(editor, charTyped, dataContext);
1237         }
1238         return;
1239       }
1240       String text = String.valueOf(charTyped);
1241       consoleView.type(editor, text);
1242     }
1243   }
1244
1245   private void type(@NotNull Editor editor, @NotNull String text) {
1246     flushDeferredText();
1247     SelectionModel selectionModel = editor.getSelectionModel();
1248
1249     int lastOffset = selectionModel.hasSelection() ? selectionModel.getSelectionStart() : editor.getCaretModel().getOffset() - 1;
1250     RangeMarker marker = findTokenMarker(lastOffset);
1251     if (getTokenType(marker) != ConsoleViewContentType.USER_INPUT) {
1252       print(text, ConsoleViewContentType.USER_INPUT);
1253       moveScrollRemoveSelection(editor, editor.getDocument().getTextLength());
1254       return;
1255     }
1256
1257     String textToUse = StringUtil.convertLineSeparators(text);
1258     int typeOffset;
1259     if (selectionModel.hasSelection()) {
1260       Document document = editor.getDocument();
1261       int start = selectionModel.getSelectionStart();
1262       int end = selectionModel.getSelectionEnd();
1263       document.deleteString(start, end);
1264       selectionModel.removeSelection();
1265       typeOffset = end;
1266     }
1267     else {
1268       typeOffset = selectionModel.hasSelection() ? selectionModel.getSelectionStart() : editor.getCaretModel().getOffset();
1269     }
1270     insertUserText(typeOffset, textToUse);
1271   }
1272
1273   private abstract static class ConsoleAction extends AnAction implements DumbAware {
1274     @Override
1275     public void actionPerformed(@NotNull AnActionEvent e) {
1276       ApplicationManager.getApplication().assertIsDispatchThread();
1277       DataContext context = e.getDataContext();
1278       ConsoleViewImpl console = getRunningConsole(context);
1279       if (console != null) {
1280         execute(console, context);
1281       }
1282     }
1283
1284     protected abstract void execute(@NotNull ConsoleViewImpl console, @NotNull DataContext context);
1285
1286     @Override
1287     public void update(@NotNull AnActionEvent e) {
1288       ConsoleViewImpl console = getRunningConsole(e.getDataContext());
1289       e.getPresentation().setEnabled(console != null);
1290     }
1291
1292     @Nullable
1293     private static ConsoleViewImpl getRunningConsole(@NotNull DataContext context) {
1294       Editor editor = CommonDataKeys.EDITOR.getData(context);
1295       if (editor != null) {
1296         ConsoleViewImpl console = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
1297         if (console != null && console.myState.isRunning()) {
1298           return console;
1299         }
1300       }
1301       return null;
1302     }
1303   }
1304
1305   private static class EnterHandler extends ConsoleAction {
1306     @Override
1307     public void execute(@NotNull ConsoleViewImpl consoleView, @NotNull DataContext context) {
1308       consoleView.print("\n", ConsoleViewContentType.USER_INPUT);
1309       consoleView.flushDeferredText();
1310       Editor editor = consoleView.myEditor;
1311       moveScrollRemoveSelection(editor, editor.getDocument().getTextLength());
1312     }
1313   }
1314
1315   private static class PasteHandler extends ConsoleAction {
1316     @Override
1317     public void execute(@NotNull ConsoleViewImpl consoleView, @NotNull DataContext context) {
1318       String text = CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor);
1319       if (text == null) return;
1320       Editor editor = consoleView.myEditor;
1321       consoleView.type(editor, text);
1322     }
1323   }
1324
1325   private static class BackSpaceHandler extends ConsoleAction {
1326     @Override
1327     public void execute(@NotNull ConsoleViewImpl consoleView, @NotNull DataContext context) {
1328       Editor editor = consoleView.myEditor;
1329
1330       if (IncrementalSearchHandler.isHintVisible(editor)) {
1331         getDefaultActionHandler().execute(editor, null, context);
1332         return;
1333       }
1334
1335       Document document = editor.getDocument();
1336       int length = document.getTextLength();
1337       if (length == 0) {
1338         return;
1339       }
1340
1341       SelectionModel selectionModel = editor.getSelectionModel();
1342       if (selectionModel.hasSelection()) {
1343         consoleView.deleteUserText(selectionModel.getSelectionStart(),
1344                                    selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
1345       }
1346       else if (editor.getCaretModel().getOffset() > 0) {
1347         consoleView.deleteUserText(editor.getCaretModel().getOffset() - 1, 1);
1348       }
1349     }
1350
1351     private static EditorActionHandler getDefaultActionHandler() {
1352       return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
1353     }
1354   }
1355
1356   private static class DeleteHandler extends ConsoleAction {
1357     @Override
1358     public void execute(@NotNull ConsoleViewImpl consoleView, @NotNull DataContext context) {
1359       Editor editor = consoleView.myEditor;
1360
1361       if (IncrementalSearchHandler.isHintVisible(editor)) {
1362         getDefaultActionHandler().execute(editor, null, context);
1363         return;
1364       }
1365
1366       consoleView.flushDeferredText();
1367       Document document = editor.getDocument();
1368       int length = document.getTextLength();
1369       if (length == 0) {
1370         return;
1371       }
1372
1373       SelectionModel selectionModel = editor.getSelectionModel();
1374       if (selectionModel.hasSelection()) {
1375         consoleView.deleteUserText(selectionModel.getSelectionStart(),
1376                                    selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
1377       }
1378       else {
1379         consoleView.deleteUserText(editor.getCaretModel().getOffset(), 1);
1380       }
1381     }
1382
1383     private static EditorActionHandler getDefaultActionHandler() {
1384       return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
1385     }
1386   }
1387
1388   private static class TabHandler extends ConsoleAction {
1389     @Override
1390     protected void execute(@NotNull ConsoleViewImpl console, @NotNull DataContext context) {
1391       console.type(console.myEditor, "\t");
1392     }
1393   }
1394
1395   @Override
1396   @NotNull
1397   public JComponent getPreferredFocusableComponent() {
1398     //ensure editor created
1399     getComponent();
1400     return myEditor.getContentComponent();
1401   }
1402
1403
1404   // navigate up/down in stack trace
1405   @Override
1406   public boolean hasNextOccurence() {
1407     return calcNextOccurrence(1) != null;
1408   }
1409
1410   @Override
1411   public boolean hasPreviousOccurence() {
1412     return calcNextOccurrence(-1) != null;
1413   }
1414
1415   @Override
1416   public OccurenceInfo goNextOccurence() {
1417     return calcNextOccurrence(1);
1418   }
1419
1420   @Nullable
1421   protected OccurenceInfo calcNextOccurrence(int delta) {
1422     if (myHyperlinks == null) {
1423       return null;
1424     }
1425
1426     return EditorHyperlinkSupport.getNextOccurrence(myEditor, delta, next -> {
1427       int offset = next.getStartOffset();
1428       scrollTo(offset);
1429       HyperlinkInfo hyperlinkInfo = EditorHyperlinkSupport.getHyperlinkInfo(next);
1430       if (hyperlinkInfo instanceof BrowserHyperlinkInfo) {
1431         return;
1432       }
1433       if (hyperlinkInfo instanceof HyperlinkInfoBase) {
1434         VisualPosition position = myEditor.offsetToVisualPosition(offset);
1435         Point point = myEditor.visualPositionToXY(new VisualPosition(position.getLine() + 1, position.getColumn()));
1436         ((HyperlinkInfoBase)hyperlinkInfo).navigate(myProject, new RelativePoint(myEditor.getContentComponent(), point));
1437       }
1438       else if (hyperlinkInfo != null) {
1439         hyperlinkInfo.navigate(myProject);
1440       }
1441     });
1442   }
1443
1444   @Override
1445   public OccurenceInfo goPreviousOccurence() {
1446     return calcNextOccurrence(-1);
1447   }
1448
1449   @NotNull
1450   @Override
1451   public String getNextOccurenceActionName() {
1452     return ExecutionBundle.message("down.the.stack.trace");
1453   }
1454
1455   @NotNull
1456   @Override
1457   public String getPreviousOccurenceActionName() {
1458     return ExecutionBundle.message("up.the.stack.trace");
1459   }
1460
1461   public void addCustomConsoleAction(@NotNull AnAction action) {
1462     customActions.add(action);
1463   }
1464
1465   @Override
1466   public AnAction @NotNull [] createConsoleActions() {
1467     //Initializing prev and next occurrences actions
1468     CommonActionsManager actionsManager = CommonActionsManager.getInstance();
1469     AnAction prevAction = actionsManager.createPrevOccurenceAction(this);
1470     prevAction.getTemplatePresentation().setText(getPreviousOccurenceActionName());
1471     AnAction nextAction = actionsManager.createNextOccurenceAction(this);
1472     nextAction.getTemplatePresentation().setText(getNextOccurenceActionName());
1473
1474     AnAction switchSoftWrapsAction = new ToggleUseSoftWrapsToolbarAction(SoftWrapAppliancePlaces.CONSOLE) {
1475       @Override
1476       protected Editor getEditor(@NotNull AnActionEvent e) {
1477         return myEditor;
1478       }
1479     };
1480     AnAction autoScrollToTheEndAction = new ScrollToTheEndToolbarAction(myEditor);
1481
1482     List<AnAction> consoleActions = new ArrayList<>();
1483     consoleActions.add(prevAction);
1484     consoleActions.add(nextAction);
1485     consoleActions.add(switchSoftWrapsAction);
1486     consoleActions.add(autoScrollToTheEndAction);
1487     consoleActions.add(ActionManager.getInstance().getAction("Print"));
1488     consoleActions.add(new ClearThisConsoleAction(this));
1489     consoleActions.addAll(customActions);
1490     List<ConsoleActionsPostProcessor> postProcessors = ConsoleActionsPostProcessor.EP_NAME.getExtensionList();
1491     AnAction[] result = consoleActions.toArray(AnAction.EMPTY_ARRAY);
1492     for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
1493       result = postProcessor.postProcess(this, result);
1494     }
1495     return result;
1496   }
1497
1498   @Override
1499   public void allowHeavyFilters() {
1500     myAllowHeavyFilters = true;
1501   }
1502
1503   @Override
1504   public void addChangeListener(@NotNull ChangeListener listener, @NotNull Disposable parent) {
1505     myListeners.add(listener);
1506     Disposer.register(parent, () -> myListeners.remove(listener));
1507   }
1508
1509   private void insertUserText(int offset, @NotNull String text) {
1510     List<Pair<String, ConsoleViewContentType>> result = myInputMessageFilter.applyFilter(text, ConsoleViewContentType.USER_INPUT);
1511     if (result == null) {
1512       doInsertUserInput(offset, text);
1513     }
1514     else {
1515       for (Pair<String, ConsoleViewContentType> pair : result) {
1516         String chunkText = pair.getFirst();
1517         ConsoleViewContentType chunkType = pair.getSecond();
1518         if (chunkType.equals(ConsoleViewContentType.USER_INPUT)) {
1519           doInsertUserInput(offset, chunkText);
1520           offset += chunkText.length();
1521         }
1522         else {
1523           print(chunkText, chunkType, null);
1524         }
1525       }
1526     }
1527   }
1528
1529   private void doInsertUserInput(int offset, @NotNull String text) {
1530     ApplicationManager.getApplication().assertIsDispatchThread();
1531     Editor editor = myEditor;
1532     Document document = editor.getDocument();
1533
1534     int oldDocLength = document.getTextLength();
1535     document.insertString(offset, text);
1536     int newStartOffset = Math.max(0,document.getTextLength() - oldDocLength + offset - text.length()); // take care of trim document
1537     int newEndOffset = document.getTextLength() - oldDocLength + offset; // take care of trim document
1538
1539     if (findTokenMarker(newEndOffset) == null) {
1540       createTokenRangeHighlighter(ConsoleViewContentType.USER_INPUT, newStartOffset, newEndOffset);
1541     }
1542
1543     moveScrollRemoveSelection(editor, newEndOffset);
1544     sendUserInput(text);
1545   }
1546
1547   private static void moveScrollRemoveSelection(@NotNull Editor editor, int offset) {
1548     editor.getCaretModel().moveToOffset(offset);
1549     editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
1550     editor.getSelectionModel().removeSelection();
1551   }
1552
1553   private void deleteUserText(int startOffset, int length) {
1554     Editor editor = myEditor;
1555     Document document = editor.getDocument();
1556
1557     RangeMarker marker = findTokenMarker(startOffset);
1558     if (getTokenType(marker) != ConsoleViewContentType.USER_INPUT) return;
1559
1560     int endOffset = startOffset + length;
1561     if (startOffset >= 0 && endOffset >= 0 && endOffset > startOffset) {
1562       document.deleteString(startOffset, endOffset);
1563     }
1564     moveScrollRemoveSelection(editor, startOffset);
1565   }
1566
1567   public boolean isRunning() {
1568     return myState.isRunning();
1569   }
1570
1571   @TestOnly
1572   @NotNull
1573   ConsoleState getState() {
1574     return myState;
1575   }
1576
1577   /**
1578    * Command line used to launch application/test from idea may be quite long.
1579    * Hence, it takes many visual lines during representation if soft wraps are enabled
1580    * or, otherwise, takes many columns and makes horizontal scrollbar thumb too small.
1581    * <p/>
1582    * Our point is to fold such long command line and represent it as a single visual line by default.
1583    */
1584   private class CommandLineFolding extends ConsoleFolding {
1585
1586     @Override
1587     public boolean shouldFoldLine(@NotNull Project project, @NotNull String line) {
1588       return line.length() >= 1000 && myState.isCommandLine(line);
1589     }
1590
1591     @Override
1592     public String getPlaceholderText(@NotNull Project project, @NotNull List<String> lines) {
1593       String text = lines.get(0);
1594
1595       int index = 0;
1596       if (text.charAt(0) == '"') {
1597         index = text.indexOf('"', 1) + 1;
1598       }
1599       if (index == 0) {
1600         for (boolean nonWhiteSpaceFound = false; index < text.length(); index++) {
1601           char c = text.charAt(index);
1602           if (c != ' ' && c != '\t') {
1603             nonWhiteSpaceFound = true;
1604             continue;
1605           }
1606           if (nonWhiteSpaceFound) {
1607             break;
1608           }
1609         }
1610       }
1611       assert index <= text.length();
1612       return text.substring(0, index) + " ...";
1613     }
1614   }
1615
1616   private class FlushRunnable implements Runnable {
1617     // Does request of this class was myFlushAlarm.addRequest()-ed but not yet executed
1618     private final AtomicBoolean requested = new AtomicBoolean();
1619     private final boolean adHoc; // true if requests of this class should not be merged (i.e they can be requested multiple times)
1620
1621     private FlushRunnable(boolean adHoc) {
1622       this.adHoc = adHoc;
1623     }
1624
1625     void queue(long delay) {
1626       if (myFlushAlarm.isDisposed()) return;
1627       if (adHoc || requested.compareAndSet(false, true)) {
1628         myFlushAlarm.addRequest(this, delay, getStateForUpdate());
1629       }
1630     }
1631     void clearRequested() {
1632       requested.set(false);
1633     }
1634
1635     @Override
1636     public final void run() {
1637       if (isDisposed()) return;
1638       // flush requires UndoManger/CommandProcessor properly initialized
1639       if (!StartupManagerEx.getInstanceEx(myProject).startupActivityPassed()) {
1640         addFlushRequest(DEFAULT_FLUSH_DELAY, FLUSH);
1641       }
1642
1643       clearRequested();
1644       doRun();
1645     }
1646
1647     protected void doRun() {
1648       flushDeferredText();
1649     }
1650   }
1651
1652   private final FlushRunnable FLUSH = new FlushRunnable(false);
1653
1654   private final class ClearRunnable extends FlushRunnable {
1655     private ClearRunnable() {
1656       super(false);
1657     }
1658
1659     @Override
1660     public void doRun() {
1661       doClear();
1662     }
1663   }
1664   private final ClearRunnable CLEAR = new ClearRunnable();
1665
1666   @NotNull
1667   public Project getProject() {
1668     return myProject;
1669   }
1670
1671   private class HyperlinkNavigationAction extends DumbAwareAction {
1672     @Override
1673     public void actionPerformed(@NotNull AnActionEvent e) {
1674       Runnable runnable = myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition());
1675       assert runnable != null;
1676       runnable.run();
1677     }
1678
1679     @Override
1680     public void update(@NotNull AnActionEvent e) {
1681       e.getPresentation().setEnabled(myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition()) != null);
1682     }
1683   }
1684
1685   @NotNull
1686   public String getText() {
1687     return myEditor.getDocument().getText();
1688   }
1689 }
1690