blob: 3a0caaa838c03592a5a032b70a70b14bef870333 [file] [log] [blame]
/*
* Copyright 2000-2011 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.intellij.execution.filters.HyperlinkInfo;
import com.intellij.execution.ui.ConsoleViewContentType;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
import static com.intellij.execution.impl.ConsoleViewImpl.HyperlinkTokenInfo;
import static com.intellij.execution.impl.ConsoleViewImpl.TokenInfo;
/**
* IJ user may want the console to use cyclic buffer, i.e. don't keep more than particular amount of symbols. So, we need
* to have a data structure that allow to achieve that. This class serves for that purpose.
* <p/>
* Not thread-safe.
* <p/>
* <b>Note:</b> basically this class consists of functionality that is cut from {@link ConsoleViewImpl} in order to make it possible
* to cover it by tests.
*
* @author Denis Zhdanov
* @since 4/5/11 5:26 PM
*/
public class ConsoleBuffer {
private static final int DEFAULT_CYCLIC_BUFFER_UNIT_SIZE = 256;
private static final boolean DEBUG_PROCESSING = false;
/**
* Buffer for deferred stdout, stderr and stdin output.
* <p/>
* Feel free to check rationale for using this approach at {@link #myCyclicBufferSize} contract.
*/
private final Deque<StringBuilder> myDeferredOutput = new ArrayDeque<StringBuilder>();
private final Set<ConsoleViewContentType> myContentTypesToNotStripOnCycling = new HashSet<ConsoleViewContentType>();
/**
* Main console usage scenario assumes the following:
* <pre>
* <ul>
* <li>
* console may be {@link ConsoleViewImpl#print(String, ConsoleViewContentType) provided} with the new text from any thread
* (e.g. separate thread is charged for reading output of java application launched under IJ. That output is provided
* to the console);
* </li>
* <li>current class flushes provided text to {@link Editor editor} used for representing it to end-user from EDT;</li>
* <li>
* dedicated buffer is kept to hold console text between the moment when it's provided to the current class
* and flush to the editor;</li>
* </ul>
* </pre>
* <p/>
* It's also possible to configure console to use cyclic buffer in order to avoid unnecessary memory consumption.
* However, that implies possibility of the following situation - console user provides it with the great number
* of small chunks of text (that is the case for junit processing). It's inappropriate to use single {@link StringBuilder} as
* a buffer then because every time we see that cyclic buffer size is exceeded and we need to cut exceeding text from buffer
* start, trailing part is moved to the zero offset. That produces extensive CPU usage in case of great number of small messages
* where every such message exceeds cyclic buffer size.
* <p/>
* That is the reason why we use data structure similar to STL deque here - we hold number of string buffers of small size instead
* of the single big buffer. That means that every 'cut at the start' operation requires much less number of trailing symbols
* to be moved. Current constant defines default size of that small buffers.
*/
private final int myCyclicBufferSize;
private final int myCyclicBufferUnitSize;
private final boolean myUseCyclicBuffer;
/**
* Holds information about number of symbols stored at {@link #myDeferredOutput} collection.
*/
private int myDeferredOutputLength;
/**
* Buffer for deferred stdin output.
* <p/>
* Is assumed to store user input data until it's delivered to the target process. That activity is driven from outside this class.
*/
private StringBuffer myDeferredUserInput = new StringBuffer();
/**
* Holds information about lexical division by offsets of the text that is not yet pushed to document.
* <p/>
* Target offsets are anchored to the {@link #myDeferredOutput deferred buffer}.
*/
private final List<TokenInfo> myDeferredTokens = new ArrayList<TokenInfo>();
private final Set<ConsoleViewContentType> myDeferredTypes = new HashSet<ConsoleViewContentType>();
public ConsoleBuffer() {
this(useCycleBuffer(), getCycleBufferSize(), DEFAULT_CYCLIC_BUFFER_UNIT_SIZE);
}
public ConsoleBuffer(boolean useCyclicBuffer, int cyclicBufferSize, int cyclicBufferUnitSize) {
myUseCyclicBuffer = useCyclicBuffer;
myCyclicBufferSize = Math.max(cyclicBufferSize, 0);
myCyclicBufferUnitSize = cyclicBufferUnitSize;
myContentTypesToNotStripOnCycling.add(ConsoleViewContentType.USER_INPUT);
}
public static boolean useCycleBuffer() {
final String useCycleBufferProperty = System.getProperty("idea.cycle.buffer.size");
return useCycleBufferProperty == null || !"disabled".equalsIgnoreCase(useCycleBufferProperty);
}
public static int getCycleBufferSize() {
final String cycleBufferSizeProperty = System.getProperty("idea.cycle.buffer.size");
if (cycleBufferSizeProperty == null) return 1024 * 1024;
try {
return Integer.parseInt(cycleBufferSizeProperty) * 1024;
}
catch (NumberFormatException e) {
return 1024 * 1024;
}
}
public boolean isUseCyclicBuffer() {
return myUseCyclicBuffer;
}
public int getCyclicBufferSize() {
return myCyclicBufferSize;
}
public boolean isEmpty() {
return myDeferredOutput.isEmpty() || (myDeferredOutput.size() == 1 && myDeferredOutput.getFirst().length() <= 0);
}
public int getLength() {
return myDeferredOutputLength;
}
public int getUserInputLength() {
return myDeferredUserInput.length();
}
public String getUserInput() {
return myDeferredUserInput.toString();
}
public List<TokenInfo> getDeferredTokens() {
return myDeferredTokens;
}
public Set<ConsoleViewContentType> getDeferredTokenTypes() {
return myDeferredTypes;
}
public Deque<StringBuilder> getDeferredOutput() {
return myDeferredOutput;
}
public String getText() {
if (myDeferredOutput.size() > 1) {
final StringBuilder buffer = new StringBuilder();
for (StringBuilder builder : myDeferredOutput) {
buffer.append(builder);
}
return buffer.toString();
}
else if (myDeferredOutput.size() == 1) {
return myDeferredOutput.getFirst().substring(0);
}
else {
return "";
}
}
/**
* This buffer automatically strips text that exceeds {@link #getCycleBufferSize() cyclic buffer size}. However, we may want
* to avoid 'significant text' stripping, i.e. don't strip the text of particular type.
* <p/>
* {@link ConsoleViewContentType#USER_INPUT} is considered to be such a type by default, however, it's possible to overwrite that
* via the current method.
*
* @param types content types that should not be stripped during the buffer's cycling
*/
public void setContentTypesToNotStripOnCycling(@NotNull Collection<ConsoleViewContentType> types) {
myContentTypesToNotStripOnCycling.clear();
myContentTypesToNotStripOnCycling.addAll(types);
}
public void clear() {
clear(true);
}
public void clear(boolean clearUserInputAsWell) {
if (myUseCyclicBuffer) {
myDeferredOutput.clear();
myDeferredOutput.add(new StringBuilder(myCyclicBufferUnitSize));
}
else {
for (StringBuilder builder : myDeferredOutput) {
builder.setLength(0);
}
}
myDeferredOutputLength = 0;
myDeferredTypes.clear();
myDeferredTokens.clear();
if (clearUserInputAsWell) {
myDeferredUserInput = new StringBuffer();
}
}
@Nullable
public String cutFirstUserInputLine() {
final String text = myDeferredUserInput.substring(0, myDeferredUserInput.length());
final int index = Math.max(text.lastIndexOf('\n'), text.lastIndexOf('\r'));
if (index < 0) {
return null;
}
final String result = text.substring(0, index + 1);
myDeferredUserInput.setLength(0);
myDeferredUserInput.append(text.substring(index + 1));
return result;
}
public void addUserText(int offset, String text) {
myDeferredUserInput.insert(offset, text);
}
public void removeUserText(int startOffset, int endOffset) {
if (startOffset >= myDeferredUserInput.length()) {
return;
}
int startToUse = Math.max(0, startOffset);
int endToUse = Math.min(myDeferredUserInput.length(), endOffset);
myDeferredUserInput.delete(startToUse, endToUse);
}
public void replaceUserText(int startOffset, int endOffset, String text) {
myDeferredUserInput.replace(startOffset, endOffset, text);
}
/**
* Asks current buffer to store given text of the given type.
*
* @param s text to store
* @param contentType type of the given text
* @param info hyperlink info for the given text (if any)
* @return text that is actually stored (there is a possible case that the buffer is full and given text's type
* is considered to have lower priority than the stored one, hence, it's better to drop given text completely
* or partially) and number of existed symbols removed during storing the given data
*/
@NotNull
public Pair<String, Integer> print(@NotNull String s, @NotNull ConsoleViewContentType contentType, @Nullable HyperlinkInfo info) {
int numberOfSymbolsToProceed = s.length();
int trimmedSymbolsNumber = myDeferredOutputLength;
if (contentType != ConsoleViewContentType.USER_INPUT) {
numberOfSymbolsToProceed = trimDeferredOutputIfNecessary(s.length());
trimmedSymbolsNumber -= myDeferredOutputLength;
}
else {
trimmedSymbolsNumber = 0;
}
if (numberOfSymbolsToProceed <= 0) {
return new Pair<String, Integer>("", 0);
}
if (numberOfSymbolsToProceed < s.length()) {
s = s.substring(s.length() - numberOfSymbolsToProceed);
}
myDeferredTypes.add(contentType);
s = StringUtil.convertLineSeparators(s, true);
myDeferredOutputLength += s.length();
StringBuilder bufferToUse;
if (myDeferredOutput.isEmpty()) {
myDeferredOutput.add(bufferToUse = new StringBuilder(myCyclicBufferUnitSize));
}
else {
bufferToUse = myDeferredOutput.getLast();
}
int offset = 0;
while (offset < s.length()) {
if (bufferToUse.length() >= myCyclicBufferUnitSize) {
myDeferredOutput.add(bufferToUse = new StringBuilder(myCyclicBufferUnitSize));
}
if (bufferToUse.length() < myCyclicBufferUnitSize) {
int numberOfSymbolsToAdd = Math.min(myCyclicBufferUnitSize - bufferToUse.length(), s.length() - offset);
bufferToUse.append(s.substring(offset, offset + numberOfSymbolsToAdd));
offset += numberOfSymbolsToAdd;
}
}
if (contentType == ConsoleViewContentType.USER_INPUT) {
myDeferredUserInput.append(s);
}
ConsoleUtil.addToken(s.length(), info, contentType, myDeferredTokens);
return new Pair<String, Integer>(s, trimmedSymbolsNumber);
}
//private void checkState() {
// int bufferOffset = 0;
// Iterator<StringBuilder> iterator = myDeferredOutput.iterator();
// StringBuilder currentBuffer = null;
// int prevTokenEnd = 0;
// for (TokenInfo token : myDeferredTokens) {
// if (prevTokenEnd != token.startOffset) {
// try {
// System.out.println("Problem detected!");
// System.in.read();
// }
// catch (IOException e) {
// e.printStackTrace();
// }
// }
// prevTokenEnd = token.endOffset;
// char c = token.contentType == ConsoleViewContentType.ERROR_OUTPUT ? '2' : '1';
// int length = token.getLength();
// if (currentBuffer == null) {
// currentBuffer = iterator.next();
// }
//
// while (length > 0) {
// if (bufferOffset == currentBuffer.length()) {
// if (!iterator.hasNext()) {
// try {
// System.out.println("Problem detected!");
// System.in.read();
// }
// catch (IOException e) {
// e.printStackTrace();
// }
// }
// currentBuffer = iterator.next();
// bufferOffset = 0;
// }
// else {
// int endOffset = Math.min(bufferOffset + length, currentBuffer.length());
// if (token.contentType == ConsoleViewContentType.NORMAL_OUTPUT || token.contentType == ConsoleViewContentType.ERROR_OUTPUT) {
// for (int i = bufferOffset; i < endOffset; i++) {
// char c1 = currentBuffer.charAt(i);
// if (c1 != c && c1 != '\n') {
// try {
// System.out.println("Problem detected!");
// System.in.read();
// }
// catch (IOException e) {
// e.printStackTrace();
// }
// }
// }
// }
// length -= endOffset - bufferOffset;
// bufferOffset = endOffset;
// }
// }
// }
//}
/**
* IJ console works as follows - it receives managed process outputs from dedicated thread that serves that process and
* pushes it to the {@link Document document} of editor used to represent process console. Important point here is that process
* output is received in a control flow of the thread over than EDT but push to the document is performed from EDT. Hence, we
* have a potential situation when particular process outputs a lot and EDT is busy or push to the document is performed slowly.
* <p/>
* We don't want to keep too many information from the underlying process then and want to trim text buffer that holds text
* to push to the document then. Current method serves exactly that purpose, i.e. it's expected to be called when new chunk of
* text is received from the underlying process and trims existing text buffer if necessary.
*
* @param numberOfNewSymbols number of symbols read from the managed process output
* @return number of newly read symbols that should be accepted
*/
@SuppressWarnings({"ForLoopReplaceableByForEach"})
private int trimDeferredOutputIfNecessary(final int numberOfNewSymbols) {
if (!myUseCyclicBuffer || myDeferredOutputLength + numberOfNewSymbols <= myCyclicBufferSize) {
return numberOfNewSymbols;
}
final int numberOfSymbolsToRemove = Math.min(myDeferredOutputLength, myDeferredOutputLength + numberOfNewSymbols - myCyclicBufferSize);
myDeferredTypes.clear();
if (DEBUG_PROCESSING) {
log("Starting console trimming. Need to delete %d symbols (deferred output length: %d, number of new symbols: %d, "
+ "cyclic buffer size: %d). Current state:",
numberOfSymbolsToRemove, myDeferredOutputLength, numberOfNewSymbols, myCyclicBufferSize
);
dumpDeferredOutput();
}
Context context = new Context(numberOfSymbolsToRemove);
TIntArrayList indicesOfTokensToRemove = new TIntArrayList();
for (int i = 0; i < myDeferredTokens.size(); i++) {
TokenInfo tokenInfo = myDeferredTokens.get(i);
tokenInfo.startOffset -= context.removedSymbolsNumber;
tokenInfo.endOffset -= context.removedSymbolsNumber;
if (!context.canContinueProcessing()) {
// Just update token offsets.
myDeferredTypes.add(tokenInfo.contentType);
if (context.removedSymbolsNumber == 0) {
break;
}
continue;
}
int tokenLength = tokenInfo.getLength();
// Don't remove input text.
if (myContentTypesToNotStripOnCycling.contains(tokenInfo.contentType)) {
skip(context, tokenLength);
myDeferredTypes.add(tokenInfo.contentType);
continue;
}
int removedTokenSymbolsNumber = remove(context, tokenLength);
if (removedTokenSymbolsNumber == tokenLength) {
indicesOfTokensToRemove.add(i);
}
else {
tokenInfo.endOffset -= removedTokenSymbolsNumber;
myDeferredTypes.add(tokenInfo.contentType);
}
}
for (int i = indicesOfTokensToRemove.size() - 1; i >= 0; i--) {
myDeferredTokens.remove(indicesOfTokensToRemove.get(i));
}
if (!myDeferredTokens.isEmpty()) {
TokenInfo tokenInfo = myDeferredTokens.get(0);
if (tokenInfo.startOffset > 0) {
final HyperlinkInfo hyperlinkInfo = tokenInfo.getHyperlinkInfo();
myDeferredTokens
.add(0, hyperlinkInfo != null ? new HyperlinkTokenInfo(ConsoleViewContentType.USER_INPUT, 0, tokenInfo.startOffset, hyperlinkInfo)
: new TokenInfo(ConsoleViewContentType.USER_INPUT, 0, tokenInfo.startOffset));
myDeferredTypes.add(ConsoleViewContentType.USER_INPUT);
}
}
if (numberOfNewSymbols + myDeferredOutputLength > myCyclicBufferSize) {
int result = myCyclicBufferSize - myDeferredOutputLength;
if (result < 0) {
return 0;
}
return result;
}
return numberOfNewSymbols;
}
private static void skip(@NotNull Context context, int symbolsToSkipNumber) {
int remainingNumberOfBufferSymbols = context.currentBuffer.length() - context.bufferOffset;
if (remainingNumberOfBufferSymbols < symbolsToSkipNumber) {
symbolsToSkipNumber -= remainingNumberOfBufferSymbols;
while (context.iterator.hasNext()) {
context.currentBuffer = context.iterator.next();
context.bufferOffset = 0;
if (DEBUG_PROCESSING) {
log("Switching to the next buffer. Number of token symbols to skip: %d", symbolsToSkipNumber);
}
if (symbolsToSkipNumber <= 0) {
break;
}
if (context.currentBuffer.length() > symbolsToSkipNumber) {
context.bufferOffset = symbolsToSkipNumber;
symbolsToSkipNumber = 0;
break;
}
else {
symbolsToSkipNumber -= context.currentBuffer.length();
}
}
assert symbolsToSkipNumber <= 0;
}
else {
context.bufferOffset += symbolsToSkipNumber;
if (DEBUG_PROCESSING) {
log("All symbols to skip are processed. Current buffer offset is %d, text: '%s'", context.bufferOffset, context.currentBuffer);
}
}
}
private int remove(@NotNull Context context, int tokenLength) {
int removedSymbolsNumber = 0;
int remainingTotalNumberOfSymbolsToRemove = context.numberOfSymbolsToRemove - context.removedSymbolsNumber;
int numberOfTokenSymbolsToRemove = Math.min(remainingTotalNumberOfSymbolsToRemove, tokenLength);
while (numberOfTokenSymbolsToRemove > 0 && context.currentBuffer != null) {
int diff = numberOfTokenSymbolsToRemove - (context.currentBuffer.length() - context.bufferOffset);
int endDeleteBufferOffset = Math.min(context.bufferOffset + numberOfTokenSymbolsToRemove, context.currentBuffer.length());
int numberOfSymbolsRemovedFromCurrentBuffer = endDeleteBufferOffset - context.bufferOffset;
if (DEBUG_PROCESSING) {
log("About to delete %d symbols from the current buffer (offset is %d). Removed symbols number: %d. Current buffer: %d: '%s'",
numberOfSymbolsRemovedFromCurrentBuffer, context.bufferOffset, context.removedSymbolsNumber, context.currentBuffer.length(),
StringUtil.convertLineSeparators(context.currentBuffer.toString()));
}
numberOfTokenSymbolsToRemove -= numberOfSymbolsRemovedFromCurrentBuffer;
removedSymbolsNumber += numberOfSymbolsRemovedFromCurrentBuffer;
context.removedSymbolsNumber += numberOfSymbolsRemovedFromCurrentBuffer;
myDeferredOutputLength -= numberOfSymbolsRemovedFromCurrentBuffer;
if (context.bufferOffset == 0 && (diff >= 0 || endDeleteBufferOffset == context.currentBuffer.length())) {
context.iterator.remove();
context.nextBuffer();
}
else {
context.currentBuffer.delete(context.bufferOffset, endDeleteBufferOffset);
if (DEBUG_PROCESSING) {
log("Removed symbols at range [%d; %d). Buffer offset: %d, buffer length: %d, text: '%s'",
context.bufferOffset, endDeleteBufferOffset, context.bufferOffset, context.currentBuffer.length(), context.currentBuffer);
}
if (context.bufferOffset == context.currentBuffer.length()) {
context.nextBuffer();
}
}
}
return removedSymbolsNumber;
}
private final class Context {
public final int numberOfSymbolsToRemove;
public StringBuilder currentBuffer;
public Iterator<StringBuilder> iterator;
public int bufferOffset;
public int removedSymbolsNumber;
Context(int numberOfSymbolsToRemove) {
this.numberOfSymbolsToRemove = numberOfSymbolsToRemove;
iterator = myDeferredOutput.iterator();
if (iterator.hasNext()) {
currentBuffer = iterator.next();
}
else {
currentBuffer = null;
}
}
public boolean canContinueProcessing() {
return removedSymbolsNumber < numberOfSymbolsToRemove && currentBuffer != null;
}
public boolean nextBuffer() {
if (iterator.hasNext()) {
currentBuffer = iterator.next();
bufferOffset = 0;
return true;
}
return false;
}
}
@SuppressWarnings({"PointlessBooleanExpression", "ConstantConditions"})
private void dumpDeferredOutput() {
if (!DEBUG_PROCESSING) {
return;
}
log("Tokens:");
for (TokenInfo token : myDeferredTokens) {
log("\t" + token);
}
log("Data:");
for (StringBuilder buffer : myDeferredOutput) {
log("\t%d: '%s'", buffer.length(), StringUtil.convertLineSeparators(buffer.toString()));
}
log("-----------------------------------------------------------------------------------------------------");
}
@SuppressWarnings({"UnusedDeclaration", "CallToPrintStackTrace"})
private static void log(Object o) {
//try {
// doLog(o);
//}
//catch (Exception e) {
// e.printStackTrace();
//}
}
@SuppressWarnings({"UnusedDeclaration", "CallToPrintStackTrace"})
private static void log(String message, Object... formatData) {
//try {
// doLog(String.format(message, formatData));
//}
//catch (Exception e) {
// e.printStackTrace();
//}
}
//private static BufferedWriter myWriter;
//private static void doLog(Object o) throws Exception {
// if (!DEBUG_PROCESSING) {
// return;
// }
// File file = new File("/home/denis/log/console.log");
// if (myWriter == null || !file.exists()) {
// myWriter = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(file)));
// }
// myWriter.write(o.toString());
// myWriter.newLine();
// myWriter.flush();
//}
}