blob: ae5cfe829f6c7cd171c11242aab63a4676e10409 [file] [log] [blame]
/*
* Copyright 2000-2014 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.intellij.execution.impl;
import com.google.common.base.CharMatcher;
import com.intellij.codeInsight.navigation.IncrementalSearchHandler;
import com.intellij.codeInsight.template.impl.editorActions.TypedActionHandlerBase;
import com.intellij.execution.ConsoleFolding;
import com.intellij.execution.ExecutionBundle;
import com.intellij.execution.actions.ConsoleActionsPostProcessor;
import com.intellij.execution.actions.EOFAction;
import com.intellij.execution.filters.*;
import com.intellij.execution.process.ProcessHandler;
import com.intellij.execution.ui.ConsoleView;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.execution.ui.ObservableConsoleView;
import com.intellij.icons.AllIcons;
import com.intellij.ide.CommonActionsManager;
import com.intellij.ide.OccurenceNavigator;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.actionSystem.*;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.*;
import com.intellij.openapi.editor.actionSystem.*;
import com.intellij.openapi.editor.actions.ScrollToTheEndToolbarAction;
import com.intellij.openapi.editor.actions.ToggleUseSoftWrapsToolbarAction;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.event.*;
import com.intellij.openapi.editor.ex.DocumentEx;
import com.intellij.openapi.editor.ex.EditorEx;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.editor.highlighter.EditorHighlighter;
import com.intellij.openapi.editor.highlighter.HighlighterClient;
import com.intellij.openapi.editor.highlighter.HighlighterIterator;
import com.intellij.openapi.editor.impl.DocumentImpl;
import com.intellij.openapi.editor.impl.softwrap.SoftWrapAppliancePlaces;
import com.intellij.openapi.editor.markup.RangeHighlighter;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.keymap.Keymap;
import com.intellij.openapi.keymap.KeymapManager;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.ui.EditorNotificationPanel;
import com.intellij.ui.awt.RelativePoint;
import com.intellij.util.Alarm;
import com.intellij.util.Consumer;
import com.intellij.util.EditorPopupHandler;
import com.intellij.util.SystemProperties;
import com.intellij.util.text.CharArrayUtil;
import com.intellij.util.ui.UIUtil;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
import java.awt.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.event.MouseEvent;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.concurrent.CopyOnWriteArraySet;
public class ConsoleViewImpl extends JPanel implements ConsoleView, ObservableConsoleView, DataProvider, OccurenceNavigator {
@NonNls private static final String CONSOLE_VIEW_POPUP_MENU = "ConsoleView.PopupMenu";
private static final Logger LOG = Logger.getInstance("#com.intellij.execution.impl.ConsoleViewImpl");
private static final int DEFAULT_FLUSH_DELAY = SystemProperties.getIntProperty("console.flush.delay.ms", 200);
private static final CharMatcher NEW_LINE_MATCHER = CharMatcher.anyOf("\n\r");
public static final Key<ConsoleViewImpl> CONSOLE_VIEW_IN_EDITOR_VIEW = Key.create("CONSOLE_VIEW_IN_EDITOR_VIEW");
static {
final EditorActionManager actionManager = EditorActionManager.getInstance();
final TypedAction typedAction = actionManager.getTypedAction();
typedAction.setupHandler(new MyTypedHandler(typedAction.getHandler()));
}
private final CommandLineFolding myCommandLineFolding = new CommandLineFolding();
private final DisposedPsiManagerCheck myPsiDisposedCheck;
private final boolean myIsViewer;
private ConsoleState myState;
private final Alarm mySpareTimeAlarm = new Alarm(this);
@Nullable
private final Alarm myHeavyAlarm;
private int myHeavyUpdateTicket;
private final Collection<ChangeListener> myListeners = new CopyOnWriteArraySet<ChangeListener>();
private final List<AnAction> customActions = new ArrayList<AnAction>();
private final ConsoleBuffer myBuffer = new ConsoleBuffer();
private boolean myUpdateFoldingsEnabled = true;
private EditorHyperlinkSupport myHyperlinks;
private MyDiffContainer myJLayeredPane;
private JPanel myMainPanel;
private final Runnable myFinishProgress;
private boolean myAllowHeavyFilters = false;
private static final int myFlushDelay = DEFAULT_FLUSH_DELAY;
private boolean myTooMuchOfOutput;
private boolean myInDocumentUpdate;
// If true, then a document is being cleared right now.
// Should be accessed in EDT only.
@SuppressWarnings("FieldAccessedSynchronizedAndUnsynchronized")
private boolean myDocumentClearing;
private int myLastAddedTextLength;
private int consoleTooMuchTextBufferRatio;
public Editor getEditor() {
return myEditor;
}
public EditorHyperlinkSupport getHyperlinks() {
return myHyperlinks;
}
public void scrollToEnd() {
if (myEditor == null) return;
myEditor.getCaretModel().moveToOffset(myEditor.getDocument().getTextLength());
}
public void foldImmediately() {
ApplicationManager.getApplication().assertIsDispatchThread();
if (!myFlushAlarm.isEmpty()) {
cancelAllFlushRequests();
new MyFlushRunnable().run();
}
myFoldingAlarm.cancelAllRequests();
myPendingFoldRegions.clear();
final FoldingModel model = myEditor.getFoldingModel();
model.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
for (FoldRegion region : model.getAllFoldRegions()) {
model.removeFoldRegion(region);
}
}
});
myFolding.clear();
updateFoldings(0, myEditor.getDocument().getLineCount() - 1, true);
}
static class TokenInfo {
final ConsoleViewContentType contentType;
int startOffset;
int endOffset;
private final TextAttributes attributes;
TokenInfo(final ConsoleViewContentType contentType, final int startOffset, final int endOffset) {
this.contentType = contentType;
this.startOffset = startOffset;
this.endOffset = endOffset;
attributes = contentType.getAttributes();
}
public int getLength() {
return endOffset - startOffset;
}
@Override
public String toString() {
return contentType + "[" + startOffset + ";" + endOffset + "]";
}
@Nullable
public HyperlinkInfo getHyperlinkInfo() {
return null;
}
}
static class HyperlinkTokenInfo extends TokenInfo {
private final HyperlinkInfo myHyperlinkInfo;
HyperlinkTokenInfo(final ConsoleViewContentType contentType, final int startOffset, final int endOffset, HyperlinkInfo hyperlinkInfo) {
super(contentType, startOffset, endOffset);
myHyperlinkInfo = hyperlinkInfo;
}
@Override
public HyperlinkInfo getHyperlinkInfo() {
return myHyperlinkInfo;
}
}
private final Project myProject;
private boolean myOutputPaused;
private EditorEx myEditor;
private final Object LOCK = new Object();
/**
* Holds number of symbols managed by the current console.
* <p/>
* Total number is assembled as a sum of symbols that are already pushed to the document and number of deferred symbols that
* are awaiting to be pushed to the document.
*/
private int myContentSize;
/**
* Holds information about lexical division by offsets of the text already pushed to document.
* <p/>
* Target offsets are anchored to the document here.
*/
private final List<TokenInfo> myTokens = new ArrayList<TokenInfo>();
private final TIntObjectHashMap<ConsoleFolding> myFolding = new TIntObjectHashMap<ConsoleFolding>();
private String myHelpId;
private final Alarm myFlushUserInputAlarm = new Alarm(Alarm.ThreadToUse.POOLED_THREAD, this);
private final Alarm myFlushAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, this);
private final Set<MyFlushRunnable> myCurrentRequests = new HashSet<MyFlushRunnable>();
protected final CompositeFilter myFilters;
@Nullable private final InputFilter myInputMessageFilter;
private final Alarm myFoldingAlarm = new Alarm(Alarm.ThreadToUse.SWING_THREAD, this);
private final List<FoldRegion> myPendingFoldRegions = new ArrayList<FoldRegion>();
public ConsoleViewImpl(final Project project, boolean viewer) {
this(project, GlobalSearchScope.allScope(project), viewer, true);
}
public ConsoleViewImpl(@NotNull final Project project,
@NotNull GlobalSearchScope searchScope,
boolean viewer,
boolean usePredefinedMessageFilter) {
this(project, searchScope, viewer,
new ConsoleState.NotStartedStated() {
@Override
public ConsoleState attachTo(ConsoleViewImpl console, ProcessHandler processHandler) {
return new ConsoleViewRunningState(console, processHandler, this, true, true);
}
},
usePredefinedMessageFilter);
}
protected ConsoleViewImpl(@NotNull final Project project,
@NotNull GlobalSearchScope searchScope,
boolean viewer,
@NotNull final ConsoleState initialState,
boolean usePredefinedMessageFilter)
{
super(new BorderLayout());
myIsViewer = viewer;
myState = initialState;
myPsiDisposedCheck = new DisposedPsiManagerCheck(project);
myProject = project;
myFilters = new CompositeFilter(project);
if (usePredefinedMessageFilter) {
for (ConsoleFilterProvider eachProvider : Extensions.getExtensions(ConsoleFilterProvider.FILTER_PROVIDERS)) {
Filter[] filters;
if (eachProvider instanceof ConsoleDependentFilterProvider) {
filters = ((ConsoleDependentFilterProvider)eachProvider).getDefaultFilters(this, project, searchScope);
}
else if (eachProvider instanceof ConsoleFilterProviderEx) {
filters = ((ConsoleFilterProviderEx)eachProvider).getDefaultFilters(project, searchScope);
}
else {
filters = eachProvider.getDefaultFilters(project);
}
for (Filter filter : filters) {
myFilters.addFilter(filter);
}
}
}
myFilters.setForceUseAllFilters(true);
myHeavyUpdateTicket = 0;
myHeavyAlarm = myFilters.isAnyHeavy() ? new Alarm(Alarm.ThreadToUse.SHARED_THREAD, this) : null;
ConsoleInputFilterProvider[] inputFilters = Extensions.getExtensions(ConsoleInputFilterProvider.INPUT_FILTER_PROVIDERS);
if (inputFilters.length > 0) {
CompositeInputFilter compositeInputFilter = new CompositeInputFilter(project);
myInputMessageFilter = compositeInputFilter;
for (ConsoleInputFilterProvider eachProvider : inputFilters) {
InputFilter[] filters = eachProvider.getDefaultFilters(project);
for (final InputFilter filter : filters) {
compositeInputFilter.addFilter(new InputFilter() {
boolean isBroken;
@Nullable
@Override
public List<Pair<String, ConsoleViewContentType>> applyFilter(String text, ConsoleViewContentType contentType) {
if (!isBroken) {
try {
return filter.applyFilter(text, contentType);
}
catch (Throwable e) {
isBroken = true;
LOG.error(e);
}
}
return null;
}
});
}
}
}
else {
myInputMessageFilter = null;
}
myFinishProgress = new Runnable() {
@Override
public void run() {
myJLayeredPane.finishUpdating();
}
};
consoleTooMuchTextBufferRatio = Registry.intValue("console.too.much.text.buffer.ratio");
}
@Override
public void attachToProcess(final ProcessHandler processHandler) {
myState = myState.attachTo(this, processHandler);
}
@Override
public void clear() {
if (myEditor == null) return;
synchronized (LOCK) {
// real document content will be cleared on next flush;
myContentSize = 0;
myBuffer.clear();
myFolding.clear();
}
if (myFlushAlarm.isDisposed()) return;
cancelAllFlushRequests();
addFlushRequest(new MyClearRunnable());
cancelHeavyAlarm();
}
@Override
public void scrollTo(final int offset) {
if (myEditor == null || myFlushAlarm.isDisposed()) return;
class ScrollRunnable extends MyFlushRunnable {
private final int myOffset = offset;
@Override
public void doRun() {
flushDeferredText();
if (myEditor == null) return;
int moveOffset = Math.min(offset, myEditor.getDocument().getTextLength());
if (myBuffer.isUseCyclicBuffer() && moveOffset >= myEditor.getDocument().getTextLength()) {
moveOffset = 0;
}
myEditor.getCaretModel().moveToOffset(moveOffset);
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
@Override
public boolean equals(Object o) {
return super.equals(o) && myOffset == ((ScrollRunnable)o).myOffset;
}
}
addFlushRequest(new ScrollRunnable());
}
public void requestScrollingToEnd() {
if (myEditor == null || myFlushAlarm.isDisposed()) {
return;
}
addFlushRequest(new MyFlushRunnable() {
@Override
public void doRun() {
flushDeferredText();
if (myEditor == null || myFlushAlarm.isDisposed()) {
return;
}
myEditor.getCaretModel().moveToOffset(myEditor.getDocument().getTextLength());
myEditor.getScrollingModel().scrollToCaret(ScrollType.MAKE_VISIBLE);
}
});
}
private void addFlushRequest(MyFlushRunnable scrollRunnable) {
addFlushRequest(scrollRunnable, 0);
}
private void addFlushRequest(MyFlushRunnable flushRunnable, final int millis) {
synchronized (myCurrentRequests) {
if (!myFlushAlarm.isDisposed() && myCurrentRequests.add(flushRunnable)) {
myFlushAlarm.addRequest(flushRunnable, millis, getStateForUpdate());
}
}
}
private static void assertIsDispatchThread() {
ApplicationManager.getApplication().assertIsDispatchThread();
}
@Override
public void setOutputPaused(final boolean value) {
myOutputPaused = value;
if (!value) {
requestFlushImmediately();
}
}
@Override
public boolean isOutputPaused() {
return myOutputPaused;
}
@Override
public boolean hasDeferredOutput() {
synchronized (LOCK) {
return myBuffer.getLength() > 0;
}
}
@Override
public void performWhenNoDeferredOutput(final Runnable runnable) {
//Q: implement in another way without timer?
if (!hasDeferredOutput()) {
runnable.run();
}
else {
performLaterWhenNoDeferredOutput(runnable);
}
}
private void performLaterWhenNoDeferredOutput(final Runnable runnable) {
if (mySpareTimeAlarm.isDisposed()) return;
mySpareTimeAlarm.addRequest(
new Runnable() {
@Override
public void run() {
performWhenNoDeferredOutput(runnable);
}
},
100,
ModalityState.stateForComponent(myJLayeredPane)
);
}
@Override
public JComponent getComponent() {
if (myMainPanel == null) {
myMainPanel = new JPanel(new BorderLayout());
myJLayeredPane = new MyDiffContainer(myMainPanel, myFilters.getUpdateMessage());
Disposer.register(this, myJLayeredPane);
add(myJLayeredPane, BorderLayout.CENTER);
}
if (myEditor == null) {
myEditor = createEditor();
registerConsoleEditorActions();
myEditor.getScrollPane().setBorder(null);
myHyperlinks = new EditorHyperlinkSupport(myEditor, myProject);
requestFlushImmediately();
myMainPanel.add(createCenterComponent(), BorderLayout.CENTER);
myEditor.getScrollingModel().addVisibleAreaListener(new VisibleAreaListener() {
@Override
public void visibleAreaChanged(VisibleAreaEvent e) {
// There is a possible case that the console text is populated while the console is not shown (e.g. we're debugging and
// 'Debugger' tab is active while 'Console' is not). It's also possible that newly added text contains long lines that
// are soft wrapped. We want to update viewport position then when the console becomes visible.
final Rectangle oldRectangle = e.getOldRectangle();
if (oldRectangle == null) {
return;
}
Editor myEditor = e.getEditor();
if (oldRectangle.height <= 0 && e.getNewRectangle().height > 0 && myEditor.getSoftWrapModel().isSoftWrappingEnabled()
&& myEditor.getCaretModel().getOffset() == myEditor.getDocument().getTextLength()) {
EditorUtil.scrollToTheEnd(myEditor);
}
}
});
}
return this;
}
protected JComponent createCenterComponent() {
return myEditor.getComponent();
}
@Override
public void dispose() {
myState = myState.dispose();
if (myEditor != null) {
cancelAllFlushRequests();
mySpareTimeAlarm.cancelAllRequests();
disposeEditor();
synchronized (LOCK) {
myBuffer.clear();
}
myEditor = null;
myHyperlinks = null;
}
}
private void cancelAllFlushRequests() {
synchronized (myCurrentRequests) {
for (MyFlushRunnable request : myCurrentRequests) {
request.invalidate();
}
myCurrentRequests.clear();
myFlushAlarm.cancelAllRequests();
}
}
protected void disposeEditor() {
UIUtil.invokeAndWaitIfNeeded(new Runnable() {
@Override
public void run() {
if (!myEditor.isDisposed()) {
EditorFactory.getInstance().releaseEditor(myEditor);
}
}
});
}
@Override
public void print(@NotNull String s, @NotNull ConsoleViewContentType contentType) {
if (myInputMessageFilter == null) {
printHyperlink(s, contentType, null);
return;
}
List<Pair<String, ConsoleViewContentType>> result = myInputMessageFilter.applyFilter(s, contentType);
if (result == null) {
printHyperlink(s, contentType, null);
}
else {
for (Pair<String, ConsoleViewContentType> pair : result) {
if (pair.first != null) {
printHyperlink(pair.first, pair.second == null ? contentType : pair.second, null);
}
}
}
}
private void printHyperlink(@NotNull String s, @NotNull ConsoleViewContentType contentType, @Nullable HyperlinkInfo info) {
synchronized (LOCK) {
Pair<String, Integer> pair = myBuffer.print(s, contentType, info);
s = pair.first;
myContentSize += s.length() - pair.second;
if (contentType == ConsoleViewContentType.USER_INPUT && NEW_LINE_MATCHER.indexIn(s) >= 0) {
flushDeferredUserInput();
}
if (myEditor != null && !myFlushAlarm.isDisposed()) {
final boolean shouldFlushNow = myBuffer.isUseCyclicBuffer() && myBuffer.getLength() >= myBuffer.getCyclicBufferSize();
addFlushRequest(new MyFlushRunnable(), shouldFlushNow ? 0 : myFlushDelay);
}
}
}
private void addToken(int length, @Nullable HyperlinkInfo info, ConsoleViewContentType contentType) {
ConsoleUtil.addToken(length, info, contentType, myTokens);
}
private static ModalityState getStateForUpdate() {
return null;//myStateForUpdate != null ? myStateForUpdate.compute() : ModalityState.stateForComponent(this);
}
private void requestFlushImmediately() {
if (myEditor != null && !myFlushAlarm.isDisposed()) {
addFlushRequest(new MyFlushRunnable());
}
}
@Override
public int getContentSize() {
synchronized (LOCK) {
return myContentSize;
}
}
@Override
public boolean canPause() {
return true;
}
public void flushDeferredText() {
flushDeferredText(false);
}
private void flushDeferredText(boolean clear) {
ApplicationManager.getApplication().assertIsDispatchThread();
if (myProject.isDisposed()) {
return;
}
EditorEx editor = myEditor;
if (editor == null) {
//already disposed
return;
}
if (clear) {
final DocumentEx document = editor.getDocument();
synchronized (LOCK) {
myTokens.clear();
clearHyperlinkAndFoldings();
}
final int documentTextLength = document.getTextLength();
if (documentTextLength > 0) {
CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
@Override
public void run() {
document.setInBulkUpdate(true);
try {
myInDocumentUpdate = true;
myDocumentClearing = true;
document.deleteString(0, documentTextLength);
}
finally {
document.setInBulkUpdate(false);
myDocumentClearing = false;
myInDocumentUpdate = false;
}
}
}, null, DocCommandGroupId.noneGroupId(document));
}
}
final String addedText;
final Collection<ConsoleViewContentType> contentTypes;
int deferredTokensSize;
synchronized (LOCK) {
if (myOutputPaused) return;
if (myBuffer.isEmpty()) return;
addedText = myBuffer.getText();
contentTypes = Collections.unmodifiableCollection(new HashSet<ConsoleViewContentType>(myBuffer.getDeferredTokenTypes()));
List<TokenInfo> deferredTokens = myBuffer.getDeferredTokens();
for (TokenInfo deferredToken : deferredTokens) {
addToken(deferredToken.getLength(), deferredToken.getHyperlinkInfo(), deferredToken.contentType);
}
deferredTokensSize = deferredTokens.size();
myBuffer.clear(false);
cancelHeavyAlarm();
}
final Document document = myEditor.getDocument();
final RangeMarker lastProcessedOutput = document.createRangeMarker(document.getTextLength(), document.getTextLength());
final int caretOffset = myEditor.getCaretModel().getOffset();
final boolean isAtLastLine = isCaretAtLastLine();
CommandProcessor.getInstance().executeCommand(myProject, new Runnable() {
@Override
public void run() {
boolean preserveCurrentVisualArea = caretOffset < document.getTextLength();
if (preserveCurrentVisualArea) {
myEditor.getScrollingModel().accumulateViewportChanges();
}
try {
myInDocumentUpdate = true;
String[] strings = addedText.split("\\r");
for (int i = 0; i < strings.length - 1; i++) {
document.insertString(document.getTextLength(), strings[i]);
int lastLine = document.getLineCount() - 1;
if (lastLine >= 0) {
ConsoleUtil.updateTokensOnTextRemoval(myTokens, document.getTextLength(), document.getTextLength() + 1);
document.deleteString(document.getLineStartOffset(lastLine), document.getTextLength());
}
}
if (strings.length > 0) {
document.insertString(document.getTextLength(), strings[strings.length - 1]);
myContentSize -= strings.length - 1;
}
}
finally {
myInDocumentUpdate = false;
if (preserveCurrentVisualArea) {
myEditor.getScrollingModel().flushViewportChanges();
}
}
if (!contentTypes.isEmpty()) {
for (ChangeListener each : myListeners) {
each.contentAdded(contentTypes);
}
}
}
}, null, DocCommandGroupId.noneGroupId(document));
synchronized (LOCK) {
for (int i = myTokens.size() - 1; i >= 0 && deferredTokensSize > 0; i--, deferredTokensSize--) {
TokenInfo token = myTokens.get(i);
final HyperlinkInfo info = token.getHyperlinkInfo();
if (info != null) {
myHyperlinks.createHyperlink(token.startOffset, token.endOffset, null, info);
}
}
}
myPsiDisposedCheck.performCheck();
myLastAddedTextLength = addedText.length();
if (!myTooMuchOfOutput) {
if (isTheAmountOfTextTooBig(myLastAddedTextLength)) { // disable hyperlinks and folding until new output arriving slows down again
myTooMuchOfOutput = true;
final EditorNotificationPanel comp =
new EditorNotificationPanel().text("Too much output to process").icon(AllIcons.General.ExclMark);
final Alarm tooMuchOutputAlarm = new Alarm();
//show the notification with a delay to avoid blinking when "too much output" ceases quickly
tooMuchOutputAlarm.addRequest(new Runnable() {
@Override
public void run() {
add(comp, BorderLayout.NORTH);
}
}, 300);
performWhenNoDeferredOutput(new Runnable() {
@Override
public void run() {
if (!isTheAmountOfTextTooBig(myLastAddedTextLength)) {
try {
highlightHyperlinksAndFoldings(lastProcessedOutput);
}
finally {
myTooMuchOfOutput = false;
remove(comp);
tooMuchOutputAlarm.cancelAllRequests();
}
}
else {
myLastAddedTextLength = 0;
performLaterWhenNoDeferredOutput(this);
}
}
});
}
else {
highlightHyperlinksAndFoldings(lastProcessedOutput);
}
}
if (isAtLastLine) {
EditorUtil.scrollToTheEnd(myEditor);
}
}
private boolean isTheAmountOfTextTooBig(final int textLength) {
return textLength > myBuffer.getCyclicBufferSize() / consoleTooMuchTextBufferRatio;
}
private void clearHyperlinkAndFoldings() {
myEditor.getMarkupModel().removeAllHighlighters();
myPendingFoldRegions.clear();
myFolding.clear();
myFoldingAlarm.cancelAllRequests();
myEditor.getFoldingModel().runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
myEditor.getFoldingModel().clearFoldRegions();
}
});
cancelHeavyAlarm();
}
private void cancelHeavyAlarm() {
if (myHeavyAlarm != null && !myHeavyAlarm.isDisposed()) {
myHeavyAlarm.cancelAllRequests();
++myHeavyUpdateTicket;
}
}
private void flushDeferredUserInput() {
final String textToSend = myBuffer.cutFirstUserInputLine();
if (textToSend == null) {
return;
}
myFlushUserInputAlarm.addRequest(new Runnable() {
@Override
public void run() {
if (myState.isRunning()) {
try {
// this may block forever, see IDEA-54340
myState.sendUserInput(textToSend);
}
catch (IOException ignored) {
}
}
}
}, 0);
}
@Override
public Object getData(final String dataId) {
if (CommonDataKeys.NAVIGATABLE.is(dataId)) {
if (myEditor == null) {
return null;
}
final LogicalPosition pos = myEditor.getCaretModel().getLogicalPosition();
final HyperlinkInfo info = myHyperlinks.getHyperlinkInfoByLineAndCol(pos.line, pos.column);
final OpenFileDescriptor openFileDescriptor = info instanceof FileHyperlinkInfo ? ((FileHyperlinkInfo)info).getDescriptor() : null;
if (openFileDescriptor == null || !openFileDescriptor.getFile().isValid()) {
return null;
}
return openFileDescriptor;
}
if (CommonDataKeys.EDITOR.is(dataId)) {
return myEditor;
}
if (PlatformDataKeys.HELP_ID.is(dataId)) {
return myHelpId;
}
if (LangDataKeys.CONSOLE_VIEW.is(dataId)) {
return this;
}
return null;
}
@Override
public void setHelpId(final String helpId) {
myHelpId = helpId;
}
public void setUpdateFoldingsEnabled(boolean updateFoldingsEnabled) {
myUpdateFoldingsEnabled = updateFoldingsEnabled;
}
@Override
public void addMessageFilter(final Filter filter) {
myFilters.addFilter(filter);
}
@Override
public void printHyperlink(final String hyperlinkText, final HyperlinkInfo info) {
printHyperlink(hyperlinkText, ConsoleViewContentType.NORMAL_OUTPUT, info);
}
private EditorEx createEditor() {
return ApplicationManager.getApplication().runReadAction(new Computable<EditorEx>() {
@Override
public EditorEx compute() {
EditorEx editor = createRealEditor();
editor.addEditorMouseListener(new EditorPopupHandler() {
@Override
public void invokePopup(final EditorMouseEvent event) {
popupInvoked(event.getMouseEvent());
}
});
editor.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
public void documentChanged(DocumentEvent event) {
onDocumentChanged(event);
}
}, ConsoleViewImpl.this);
int bufferSize = myBuffer.isUseCyclicBuffer() ? myBuffer.getCyclicBufferSize() : 0;
editor.getDocument().setCyclicBufferSize(bufferSize);
editor.putUserData(CONSOLE_VIEW_IN_EDITOR_VIEW, ConsoleViewImpl.this);
editor.getSettings().setAllowSingleLogicalLineFolding(true); // We want to fold long soft-wrapped command lines
editor.setHighlighter(createHighlighter());
return editor;
}
});
}
private void onDocumentChanged(DocumentEvent event) {
if (event.getNewLength() == 0) {
// string has been removed, adjust token ranges
synchronized (LOCK) {
ConsoleUtil.updateTokensOnTextRemoval(myTokens, event.getOffset(), event.getOffset() + event.getOldLength());
int toRemoveLen = event.getOldLength();
if (!myDocumentClearing) {
// If document is being cleared now, then this event has been occurred as a result of calling clear() method.
// At start clear() method sets 'myContentSize' to 0, so there is no need to perform update again.
// Moreover, performing update of 'myContentSize' breaks executing "console.print();" immediately after "console.clear();".
myContentSize -= Math.min(myContentSize, toRemoveLen);
}
}
}
else if (!myInDocumentUpdate) {
int newFragmentLength = event.getNewFragment().length();
// track external appends
if (event.getOldFragment().length() == 0 && newFragmentLength > 0) {
synchronized (LOCK) {
myContentSize += newFragmentLength;
addToken(newFragmentLength, null, ConsoleViewContentType.NORMAL_OUTPUT);
}
}
else {
LOG.warn("unhandled external change: " + event);
}
}
}
protected EditorEx createRealEditor() {
return ConsoleViewUtil.setupConsoleEditor(myProject, true, false);
}
protected MyHighlighter createHighlighter() {
return new MyHighlighter();
}
private void registerConsoleEditorActions() {
HyperlinkNavigationAction hyperlinkNavigationAction = new HyperlinkNavigationAction();
hyperlinkNavigationAction.registerCustomShortcutSet(CommonShortcuts.ENTER, myEditor.getContentComponent());
registerActionHandler(myEditor, IdeActions.ACTION_GOTO_DECLARATION, hyperlinkNavigationAction);
if (!myIsViewer) {
new EnterHandler().registerCustomShortcutSet(CommonShortcuts.ENTER, myEditor.getContentComponent());
registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_PASTE, new PasteHandler());
registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_BACKSPACE, new BackSpaceHandler());
registerActionHandler(myEditor, IdeActions.ACTION_EDITOR_DELETE, new DeleteHandler());
registerActionHandler(myEditor, EOFAction.ACTION_ID, ActionManager.getInstance().getAction(EOFAction.ACTION_ID));
}
}
private static void registerActionHandler(final Editor editor, final String actionId, final AnAction action) {
final Keymap keymap = KeymapManager.getInstance().getActiveKeymap();
final Shortcut[] shortcuts = keymap.getShortcuts(actionId);
action.registerCustomShortcutSet(new CustomShortcutSet(shortcuts), editor.getContentComponent());
}
private void popupInvoked(MouseEvent mouseEvent) {
final ActionManager actionManager = ActionManager.getInstance();
final HyperlinkInfo info = myHyperlinks != null ? myHyperlinks.getHyperlinkInfoByPoint(mouseEvent.getPoint()) : null;
ActionGroup group = null;
if (info instanceof HyperlinkWithPopupMenuInfo) {
group = ((HyperlinkWithPopupMenuInfo)info).getPopupMenuGroup(mouseEvent);
}
if (group == null) {
group = (ActionGroup)actionManager.getAction(CONSOLE_VIEW_POPUP_MENU);
}
final ConsoleActionsPostProcessor[] postProcessors = Extensions.getExtensions(ConsoleActionsPostProcessor.EP_NAME);
AnAction[] result = group.getChildren(null);
for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
result = postProcessor.postProcessPopupActions(this, result);
}
final DefaultActionGroup processedGroup = new DefaultActionGroup(result);
final ActionPopupMenu menu = actionManager.createActionPopupMenu(ActionPlaces.EDITOR_POPUP, processedGroup);
menu.getComponent().show(mouseEvent.getComponent(), mouseEvent.getX(), mouseEvent.getY());
}
private void highlightHyperlinksAndFoldings(RangeMarker lastProcessedOutput) {
boolean canHighlightHyperlinks = !myFilters.isEmpty() || !myFilters.isEmpty();
if (!canHighlightHyperlinks && myUpdateFoldingsEnabled) {
return;
}
final int line1 = lastProcessedOutput.isValid() ? myEditor.getDocument().getLineNumber(lastProcessedOutput.getEndOffset()) : 0;
lastProcessedOutput.dispose();
int endLine = myEditor.getDocument().getLineCount() - 1;
ApplicationManager.getApplication().assertIsDispatchThread();
PsiDocumentManager.getInstance(myProject).commitAllDocuments();
if (canHighlightHyperlinks) {
myHyperlinks.highlightHyperlinks(myFilters, line1, endLine);
}
if (myAllowHeavyFilters && myFilters.isAnyHeavy() && myFilters.shouldRunHeavy()) {
runHeavyFilters(line1, endLine);
}
if (myUpdateFoldingsEnabled) {
updateFoldings(line1, endLine, true);
}
}
private void runHeavyFilters(int line1, int endLine) {
final int startLine = Math.max(0, line1);
final Document document = myEditor.getDocument();
final int startOffset = document.getLineStartOffset(startLine);
String text = document.getText(new TextRange(startOffset, document.getLineEndOffset(endLine)));
final Document documentCopy = new DocumentImpl(text,true);
documentCopy.setReadOnly(true);
myJLayeredPane.startUpdating();
final int currentValue = myHeavyUpdateTicket;
assert myHeavyAlarm != null;
myHeavyAlarm.addRequest(new Runnable() {
@Override
public void run() {
if (!myFilters.shouldRunHeavy()) return;
try {
myFilters.applyHeavyFilter(documentCopy, startOffset, startLine, new Consumer<FilterMixin.AdditionalHighlight>() {
@Override
public void consume(final FilterMixin.AdditionalHighlight additionalHighlight) {
if (myFlushAlarm.isDisposed()) return;
addFlushRequest(new MyFlushRunnable() {
@Override
public void doRun() {
if (myHeavyUpdateTicket != currentValue) return;
myHyperlinks.addHighlighter(additionalHighlight.getStart(), additionalHighlight.getEnd(),
additionalHighlight.getTextAttributes(null));
}
@Override
public boolean equals(Object o) {
return this == o && super.equals(o);
}
});
}
});
}
finally {
if (myHeavyAlarm.isEmpty()) {
SwingUtilities.invokeLater(myFinishProgress);
}
}
}
}, 0);
}
private void updateFoldings(final int line1, final int endLine, boolean immediately) {
final Document document = myEditor.getDocument();
final CharSequence chars = document.getCharsSequence();
final int startLine = Math.max(0, line1);
final List<FoldRegion> toAdd = new ArrayList<FoldRegion>();
for (int line = startLine; line <= endLine; line++) {
addFolding(document, chars, line, toAdd);
}
if (!toAdd.isEmpty()) {
doUpdateFolding(toAdd, immediately);
}
}
private void doUpdateFolding(final List<FoldRegion> toAdd, final boolean immediately) {
assertIsDispatchThread();
myPendingFoldRegions.addAll(toAdd);
myFoldingAlarm.cancelAllRequests();
final Runnable runnable = new Runnable() {
@Override
public void run() {
if (myEditor == null || myEditor.isDisposed()) {
return;
}
assertIsDispatchThread();
final FoldingModel model = myEditor.getFoldingModel();
final Runnable operation = new Runnable() {
@Override
public void run() {
assertIsDispatchThread();
for (FoldRegion region : myPendingFoldRegions) {
region.setExpanded(false);
model.addFoldRegion(region);
}
myPendingFoldRegions.clear();
}
};
if (immediately) {
model.runBatchFoldingOperation(operation);
if (isCaretAtLastLine()) {
EditorUtil.scrollToTheEnd(myEditor);
}
}
else {
model.runBatchFoldingOperationDoNotCollapseCaret(operation);
}
}
};
if (immediately || myPendingFoldRegions.size() > 100) {
runnable.run();
}
else {
myFoldingAlarm.addRequest(runnable, 50);
}
}
private boolean isCaretAtLastLine() {
final Document document = myEditor.getDocument();
final int caretOffset = myEditor.getCaretModel().getOffset();
return document.getLineNumber(caretOffset) >= document.getLineCount() - 1;
}
private void addFolding(Document document, CharSequence chars, int line, List<FoldRegion> toAdd) {
String commandLinePlaceholder = myCommandLineFolding.getPlaceholder(line);
if (commandLinePlaceholder != null) {
FoldRegion region = myEditor.getFoldingModel().createFoldRegion(
document.getLineStartOffset(line), document.getLineEndOffset(line), commandLinePlaceholder, null, false
);
toAdd.add(region);
return;
}
ConsoleFolding current = foldingForLine(EditorHyperlinkSupport.getLineText(document, line, false));
if (current != null) {
myFolding.put(line, current);
}
final ConsoleFolding prevFolding = myFolding.get(line - 1);
if (current == null && prevFolding != null) {
final int lEnd = line - 1;
int lStart = lEnd;
while (prevFolding.equals(myFolding.get(lStart - 1))) lStart--;
for (int i = lStart; i <= lEnd; i++) {
myFolding.remove(i);
}
List<String> toFold = new ArrayList<String>(lEnd - lStart + 1);
for (int i = lStart; i <= lEnd; i++) {
toFold.add(EditorHyperlinkSupport.getLineText(document, i, false));
}
int oStart = document.getLineStartOffset(lStart);
if (oStart > 0) oStart--;
int oEnd = CharArrayUtil.shiftBackward(chars, document.getLineEndOffset(lEnd) - 1, " \t") + 1;
String placeholder = prevFolding.getPlaceholderText(toFold);
FoldRegion region = placeholder == null ? null : myEditor.getFoldingModel().createFoldRegion(oStart, oEnd, placeholder, null, false);
if (region != null) {
toAdd.add(region);
}
}
}
@Nullable
private static ConsoleFolding foldingForLine(String lineText) {
for (ConsoleFolding folding : ConsoleFolding.EP_NAME.getExtensions()) {
if (folding.shouldFoldLine(lineText)) {
return folding;
}
}
return null;
}
public static class ClearAllAction extends DumbAwareAction {
public ClearAllAction() {
super(ExecutionBundle.message("clear.all.from.console.action.name"), "Clear the contents of the console", AllIcons.Actions.GC);
}
@Override
public void update(AnActionEvent e) {
boolean enabled = e.getData(LangDataKeys.CONSOLE_VIEW) != null;
Editor editor = e.getData(CommonDataKeys.EDITOR);
if (editor != null && editor.getDocument().getTextLength() == 0) {
enabled = false;
}
e.getPresentation().setEnabled(enabled);
}
@Override
public void actionPerformed(final AnActionEvent e) {
final ConsoleView consoleView = e.getData(LangDataKeys.CONSOLE_VIEW);
if (consoleView != null) {
consoleView.clear();
}
}
}
private class MyHighlighter extends DocumentAdapter implements EditorHighlighter {
private HighlighterClient myEditor;
@NotNull
@Override
public HighlighterIterator createIterator(final int startOffset) {
final int startIndex = ConsoleUtil.findTokenInfoIndexByOffset(myTokens, startOffset);
return new HighlighterIterator() {
private int myIndex = startIndex;
@Override
public TextAttributes getTextAttributes() {
return getTokenInfo() == null ? null : getTokenInfo().attributes;
}
@Override
public int getStart() {
return getTokenInfo() == null ? 0 : getTokenInfo().startOffset;
}
@Override
public int getEnd() {
return getTokenInfo() == null ? 0 : getTokenInfo().endOffset;
}
@Override
public IElementType getTokenType() {
return null;
}
@Override
public void advance() {
myIndex++;
}
@Override
public void retreat() {
myIndex--;
}
@Override
public boolean atEnd() {
return myIndex < 0 || myIndex >= myTokens.size();
}
@Override
public Document getDocument() {
return myEditor.getDocument();
}
private TokenInfo getTokenInfo() {
return myTokens.get(myIndex);
}
};
}
@Override
public void setText(@NotNull final CharSequence text) {
}
@Override
public void setEditor(@NotNull final HighlighterClient editor) {
LOG.assertTrue(myEditor == null, "Highlighters cannot be reused with different editors");
myEditor = editor;
}
@Override
public void setColorScheme(@NotNull EditorColorsScheme scheme) {
}
}
private static class MyTypedHandler extends TypedActionHandlerBase {
private MyTypedHandler(final TypedActionHandler originalAction) {
super(originalAction);
}
@Override
public void execute(@NotNull final Editor editor, final char charTyped, @NotNull final DataContext dataContext) {
final ConsoleViewImpl consoleView = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
if (consoleView == null || !consoleView.myState.isRunning() || consoleView.myIsViewer) {
if (myOriginalHandler != null) myOriginalHandler.execute(editor, charTyped, dataContext);
}
else {
final String s = String.valueOf(charTyped);
SelectionModel selectionModel = editor.getSelectionModel();
if (selectionModel.hasSelection()) {
consoleView.replaceUserText(s, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
}
else {
consoleView.insertUserText(s, editor.getCaretModel().getOffset());
}
}
}
}
private abstract static class ConsoleAction extends AnAction implements DumbAware {
@Override
public void actionPerformed(final AnActionEvent e) {
final DataContext context = e.getDataContext();
final ConsoleViewImpl console = getRunningConsole(context);
execute(console, context);
}
protected abstract void execute(ConsoleViewImpl console, final DataContext context);
@Override
public void update(final AnActionEvent e) {
final ConsoleViewImpl console = getRunningConsole(e.getDataContext());
e.getPresentation().setEnabled(console != null);
}
@Nullable
private static ConsoleViewImpl getRunningConsole(final DataContext context) {
final Editor editor = CommonDataKeys.EDITOR.getData(context);
if (editor != null) {
final ConsoleViewImpl console = editor.getUserData(CONSOLE_VIEW_IN_EDITOR_VIEW);
if (console != null && console.myState.isRunning()) {
return console;
}
}
return null;
}
}
private static class EnterHandler extends ConsoleAction {
@Override
public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
consoleView.print("\n", ConsoleViewContentType.USER_INPUT);
consoleView.flushDeferredText();
final Editor editor = consoleView.myEditor;
editor.getCaretModel().moveToOffset(editor.getDocument().getTextLength());
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
}
private static class PasteHandler extends ConsoleAction {
@Override
public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
String s = CopyPasteManager.getInstance().getContents(DataFlavor.stringFlavor);
if (s == null) return;
ApplicationManager.getApplication().assertIsDispatchThread();
Editor editor = consoleView.myEditor;
SelectionModel selectionModel = editor.getSelectionModel();
if (selectionModel.hasSelection()) {
consoleView.replaceUserText(s, selectionModel.getSelectionStart(), selectionModel.getSelectionEnd());
}
else {
consoleView.insertUserText(s, editor.getCaretModel().getOffset());
}
}
}
private static class BackSpaceHandler extends ConsoleAction {
@Override
public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
final Editor editor = consoleView.myEditor;
if (IncrementalSearchHandler.isHintVisible(editor)) {
getDefaultActionHandler().execute(editor, context);
return;
}
final Document document = editor.getDocument();
final int length = document.getTextLength();
if (length == 0) {
return;
}
ApplicationManager.getApplication().assertIsDispatchThread();
SelectionModel selectionModel = editor.getSelectionModel();
if (selectionModel.hasSelection()) {
consoleView.deleteUserText(selectionModel.getSelectionStart(),
selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
}
else if (editor.getCaretModel().getOffset() > 0) {
consoleView.deleteUserText(editor.getCaretModel().getOffset() - 1, 1);
}
}
private static EditorActionHandler getDefaultActionHandler() {
return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
}
}
private static class DeleteHandler extends ConsoleAction {
@Override
public void execute(final ConsoleViewImpl consoleView, final DataContext context) {
final Editor editor = consoleView.myEditor;
if (IncrementalSearchHandler.isHintVisible(editor)) {
getDefaultActionHandler().execute(editor, context);
return;
}
final Document document = editor.getDocument();
final int length = document.getTextLength();
if (length == 0) {
return;
}
ApplicationManager.getApplication().assertIsDispatchThread();
SelectionModel selectionModel = editor.getSelectionModel();
if (selectionModel.hasSelection()) {
consoleView.deleteUserText(selectionModel.getSelectionStart(),
selectionModel.getSelectionEnd() - selectionModel.getSelectionStart());
}
else {
consoleView.deleteUserText(editor.getCaretModel().getOffset(), 1);
}
}
private static EditorActionHandler getDefaultActionHandler() {
return EditorActionManager.getInstance().getActionHandler(IdeActions.ACTION_EDITOR_BACKSPACE);
}
}
@Override
public JComponent getPreferredFocusableComponent() {
//ensure editor created
getComponent();
return myEditor.getContentComponent();
}
// navigate up/down in stack trace
@Override
public boolean hasNextOccurence() {
return calcNextOccurrence(1) != null;
}
@Override
public boolean hasPreviousOccurence() {
return calcNextOccurrence(-1) != null;
}
@Override
public OccurenceInfo goNextOccurence() {
return calcNextOccurrence(1);
}
@Nullable
protected OccurenceInfo calcNextOccurrence(final int delta) {
final EditorHyperlinkSupport hyperlinks = myHyperlinks;
if (hyperlinks == null) {
return null;
}
return EditorHyperlinkSupport.getNextOccurrence(myEditor, delta, new Consumer<RangeHighlighter>() {
@Override
public void consume(RangeHighlighter next) {
int offset = next.getStartOffset();
scrollTo(offset);
final HyperlinkInfo hyperlinkInfo = EditorHyperlinkSupport.getHyperlinkInfo(next);
if (hyperlinkInfo instanceof HyperlinkInfoBase) {
VisualPosition position = myEditor.offsetToVisualPosition(offset);
Point point = myEditor.visualPositionToXY(new VisualPosition(position.getLine() + 1, position.getColumn()));
((HyperlinkInfoBase)hyperlinkInfo).navigate(myProject, new RelativePoint(myEditor.getContentComponent(), point));
}
else if (hyperlinkInfo != null) {
hyperlinkInfo.navigate(myProject);
}
}
});
}
@Override
public OccurenceInfo goPreviousOccurence() {
return calcNextOccurrence(-1);
}
@Override
public String getNextOccurenceActionName() {
return ExecutionBundle.message("down.the.stack.trace");
}
@Override
public String getPreviousOccurenceActionName() {
return ExecutionBundle.message("up.the.stack.trace");
}
public void addCustomConsoleAction(@NotNull AnAction action) {
customActions.add(action);
}
@Override
@NotNull
public AnAction[] createConsoleActions() {
//Initializing prev and next occurrences actions
final CommonActionsManager actionsManager = CommonActionsManager.getInstance();
final AnAction prevAction = actionsManager.createPrevOccurenceAction(this);
prevAction.getTemplatePresentation().setText(getPreviousOccurenceActionName());
final AnAction nextAction = actionsManager.createNextOccurenceAction(this);
nextAction.getTemplatePresentation().setText(getNextOccurenceActionName());
final AnAction switchSoftWrapsAction = new ToggleUseSoftWrapsToolbarAction(SoftWrapAppliancePlaces.CONSOLE) {
/**
* There is a possible case that more than console is open and user toggles soft wraps mode at one of them. We want
* to update another console(s) representation as well when they are switched on after that. Hence, we remember last
* used soft wraps mode and perform update if we see that the current value differs from the stored.
*/
private boolean myLastIsSelected;
@Override
protected Editor getEditor(AnActionEvent e) {
return myEditor;
}
@Override
public boolean isSelected(AnActionEvent e) {
boolean result = super.isSelected(e);
if (result ^ myLastIsSelected) {
setSelected(null, result);
}
return myLastIsSelected = result;
}
@Override
public void setSelected(AnActionEvent e, final boolean state) {
super.setSelected(e, state);
if (myEditor == null) {
return;
}
final String placeholder = myCommandLineFolding.getPlaceholder(0);
final FoldingModel foldingModel = myEditor.getFoldingModel();
final int firstLineEnd = myEditor.getDocument().getLineEndOffset(0);
foldingModel.runBatchFoldingOperation(new Runnable() {
@Override
public void run() {
FoldRegion[] regions = foldingModel.getAllFoldRegions();
if (regions.length > 0 && regions[0].getStartOffset() == 0 && regions[0].getEndOffset() == firstLineEnd) {
foldingModel.removeFoldRegion(regions[0]);
}
if (placeholder != null) {
FoldRegion foldRegion = foldingModel.addFoldRegion(0, firstLineEnd, placeholder);
if (foldRegion != null) {
foldRegion.setExpanded(false);
}
}
}
});
}
};
final AnAction autoScrollToTheEndAction = new ScrollToTheEndToolbarAction(myEditor);
//Initializing custom actions
final AnAction[] consoleActions = new AnAction[6 + customActions.size()];
consoleActions[0] = prevAction;
consoleActions[1] = nextAction;
consoleActions[2] = switchSoftWrapsAction;
consoleActions[3] = autoScrollToTheEndAction;
consoleActions[4] = ActionManager.getInstance().getAction("Print");
consoleActions[5] = new ClearAllAction();
for (int i = 0; i < customActions.size(); ++i) {
consoleActions[i + 6] = customActions.get(i);
}
ConsoleActionsPostProcessor[] postProcessors = Extensions.getExtensions(ConsoleActionsPostProcessor.EP_NAME);
AnAction[] result = consoleActions;
for (ConsoleActionsPostProcessor postProcessor : postProcessors) {
result = postProcessor.postProcess(this, result);
}
return result;
}
@Override
public void allowHeavyFilters() {
myAllowHeavyFilters = true;
}
@Override
public void addChangeListener(@NotNull final ChangeListener listener, @NotNull final Disposable parent) {
myListeners.add(listener);
Disposer.register(parent, new Disposable() {
@Override
public void dispose() {
myListeners.remove(listener);
}
});
}
/**
* insert text to document
*
* @param s inserted text
* @param offset relatively to all document text
*/
private void insertUserText(final String s, int offset) {
ApplicationManager.getApplication().assertIsDispatchThread();
final ConsoleViewImpl consoleView = this;
final ConsoleBuffer buffer = consoleView.myBuffer;
final Editor editor = consoleView.myEditor;
final Document document = editor.getDocument();
final int startOffset;
String textToUse = StringUtil.convertLineSeparators(s);
synchronized (consoleView.LOCK) {
if (consoleView.myTokens.isEmpty()) return;
final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
if (info.contentType != ConsoleViewContentType.USER_INPUT && !StringUtil.containsChar(textToUse, '\n')) {
consoleView.print(textToUse, ConsoleViewContentType.USER_INPUT);
consoleView.flushDeferredText();
editor.getCaretModel().moveToOffset(document.getTextLength());
editor.getSelectionModel().removeSelection();
return;
}
if (info.contentType != ConsoleViewContentType.USER_INPUT) {
insertUserText("temp", offset);
final TokenInfo newInfo = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
replaceUserText(textToUse, newInfo.startOffset, newInfo.endOffset);
return;
}
final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
if (offset > info.endOffset) {
startOffset = info.endOffset;
}
else {
startOffset = Math.max(deferredOffset, Math.max(info.startOffset, offset));
}
buffer.addUserText(startOffset - deferredOffset, textToUse);
int charCountToAdd = textToUse.length();
info.endOffset += charCountToAdd;
consoleView.myContentSize += charCountToAdd;
}
try {
myInDocumentUpdate = true;
document.insertString(startOffset, textToUse);
}
finally {
myInDocumentUpdate = false;
}
// Math.max is needed when cyclic buffer is used
editor.getCaretModel().moveToOffset(Math.min(startOffset + textToUse.length(), document.getTextLength()));
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
}
/**
* replace text
*
* @param s text for replace
* @param start relatively to all document text
* @param end relatively to all document text
*/
private void replaceUserText(final String s, int start, int end) {
if (start == end) {
insertUserText(s, start);
return;
}
final ConsoleViewImpl consoleView = this;
final ConsoleBuffer buffer = consoleView.myBuffer;
final Editor editor = consoleView.myEditor;
final Document document = editor.getDocument();
final int startOffset;
final int endOffset;
synchronized (consoleView.LOCK) {
if (consoleView.myTokens.isEmpty()) return;
final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
if (info.contentType != ConsoleViewContentType.USER_INPUT) {
consoleView.print(s, ConsoleViewContentType.USER_INPUT);
consoleView.flushDeferredText();
editor.getCaretModel().moveToOffset(document.getTextLength());
editor.getSelectionModel().removeSelection();
return;
}
if (buffer.getUserInputLength() <= 0) return;
final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
startOffset = getStartOffset(start, info, deferredOffset);
endOffset = getEndOffset(end, info);
if (startOffset == -1 ||
endOffset == -1 ||
endOffset <= startOffset) {
editor.getSelectionModel().removeSelection();
editor.getCaretModel().moveToOffset(start);
return;
}
int charCountToReplace = s.length() - endOffset + startOffset;
buffer.replaceUserText(startOffset - deferredOffset, endOffset - deferredOffset, s);
info.endOffset += charCountToReplace;
if (info.startOffset == info.endOffset) {
consoleView.myTokens.remove(consoleView.myTokens.size() - 1);
}
consoleView.myContentSize += charCountToReplace;
}
try {
myInDocumentUpdate = true;
document.replaceString(startOffset, endOffset, s);
}
finally {
myInDocumentUpdate = false;
}
editor.getCaretModel().moveToOffset(startOffset + s.length());
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
}
/**
* delete text
*
* @param offset relatively to all document text
* @param length length of deleted text
*/
private void deleteUserText(int offset, int length) {
ConsoleViewImpl consoleView = this;
ConsoleBuffer buffer = consoleView.myBuffer;
final Editor editor = consoleView.myEditor;
final Document document = editor.getDocument();
final int startOffset;
final int endOffset;
synchronized (consoleView.LOCK) {
if (consoleView.myTokens.isEmpty()) return;
final TokenInfo info = consoleView.myTokens.get(consoleView.myTokens.size() - 1);
if (info.contentType != ConsoleViewContentType.USER_INPUT) return;
if (myBuffer.getUserInputLength() == 0) return;
final int deferredOffset = myContentSize - buffer.getLength() - buffer.getUserInputLength();
startOffset = getStartOffset(offset, info, deferredOffset);
endOffset = getEndOffset(offset + length, info);
if (startOffset == -1 ||
endOffset == -1 ||
endOffset <= startOffset ||
startOffset < deferredOffset) {
editor.getSelectionModel().removeSelection();
editor.getCaretModel().moveToOffset(offset);
return;
}
buffer.removeUserText(startOffset - deferredOffset, endOffset - deferredOffset);
}
try {
myInDocumentUpdate = true;
document.deleteString(startOffset, endOffset);
}
finally {
myInDocumentUpdate = false;
}
editor.getCaretModel().moveToOffset(startOffset);
editor.getScrollingModel().scrollToCaret(ScrollType.RELATIVE);
editor.getSelectionModel().removeSelection();
}
//util methods for add, replace, delete methods
private static int getStartOffset(int offset, TokenInfo info, int deferredOffset) {
int startOffset;
if (offset >= info.startOffset && offset < info.endOffset) {
startOffset = Math.max(offset, deferredOffset);
}
else if (offset < info.startOffset) {
startOffset = Math.max(info.startOffset, deferredOffset);
}
else {
startOffset = -1;
}
return startOffset;
}
private static int getEndOffset(int offset, TokenInfo info) {
int endOffset;
if (offset > info.endOffset) {
endOffset = info.endOffset;
}
else if (offset <= info.startOffset) {
endOffset = -1;
}
else {
endOffset = offset;
}
return endOffset;
}
public boolean isRunning() {
return myState.isRunning();
}
/**
* Command line used to launch application/test from idea may be quite long.
* Hence, it takes many visual lines during representation if soft wraps are enabled
* or, otherwise, takes many columns and makes horizontal scrollbar thumb too small.
* <p/>
* Our point is to fold such long command line and represent it as a single visual line by default.
*/
private class CommandLineFolding extends ConsoleFolding {
/**
* Checks if target line should be folded and returns its placeholder if the examination succeeds.
*
* @param line index of line to check
* @return placeholder text if given line should be folded; <code>null</code> otherwise
*/
@Nullable
public String getPlaceholder(int line) {
if (myEditor == null || line != 0) {
return null;
}
String text = EditorHyperlinkSupport.getLineText(myEditor.getDocument(), 0, false);
// Don't fold the first line if the line is not that big.
if (text.length() < 1000) {
return null;
}
boolean nonWhiteSpaceFound = false;
int index = 0;
for (; index < text.length(); index++) {
char c = text.charAt(index);
if (c != ' ' && c != '\t') {
nonWhiteSpaceFound = true;
continue;
}
if (nonWhiteSpaceFound) {
break;
}
}
assert index <= text.length();
return text.substring(0, index) + " ...";
}
@Override
public boolean shouldFoldLine(String line) {
return false;
}
@Override
public String getPlaceholderText(List<String> lines) {
// Is not expected to be called.
return "<...>";
}
}
private class MyFlushRunnable implements Runnable {
private volatile boolean myValid = true;
@Override
public final void run() {
synchronized (myCurrentRequests) {
myCurrentRequests.remove(this);
}
if (myValid) {
doRun();
}
}
protected void doRun() {
flushDeferredText();
}
public void invalidate() {
myValid = false;
}
public boolean isValid() {
return myValid;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MyFlushRunnable runnable = (MyFlushRunnable)o;
return myValid == runnable.myValid;
}
@Override
public int hashCode() {
return getClass().hashCode();
}
}
private final class MyClearRunnable extends MyFlushRunnable {
@Override
public void doRun() {
flushDeferredText(true);
}
}
@NotNull
public Project getProject() {
return myProject;
}
private class HyperlinkNavigationAction extends DumbAwareAction {
@Override
public void actionPerformed(AnActionEvent e) {
Runnable runnable = myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition());
assert runnable != null;
runnable.run();
}
@Override
public void update(AnActionEvent e) {
e.getPresentation().setEnabled(myHyperlinks.getLinkNavigationRunnable(myEditor.getCaretModel().getLogicalPosition()) != null);
}
}
}