IDEA-137769 ANSI colors are not interpreted on System.err

This commit is contained in:
Sergey Simonchik
2017-10-02 15:15:29 +03:00
parent 19551bd6af
commit 32d0f1de94
5 changed files with 170 additions and 50 deletions
@@ -19,6 +19,7 @@ import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.LineSeparator;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -37,9 +38,10 @@ public class AnsiEscapeDecoder {
private static final char BACKSPACE = '\b';
private final ColoredOutputTypeRegistry myColoredOutputTypeRegistry = ColoredOutputTypeRegistry.getInstance();
private Key myCurrentTextAttributes;
private String myUnhandledStdout;
private String myUnhandledStderr;
private ProcessOutputType myCurrentStdoutOutputType;
private ProcessOutputType myCurrentStderrOutputType;
/**
* Parses ansi-color codes from text and sends text fragments with color attributes to textAcceptor
@@ -78,7 +80,13 @@ public class AnsiEscapeDecoder {
// this is a simple fix for RUBY-8996:
// we replace several consecutive escape sequences with one which contains all these sequences
String colorAttribute = StringUtil.replace(escSeq, M_CSI, ";");
myCurrentTextAttributes = myColoredOutputTypeRegistry.getOutputKey(colorAttribute);
ProcessOutputType resultType = myColoredOutputTypeRegistry.getOutputType(colorAttribute, outputType);
if (resultType.isStdout()) {
myCurrentStdoutOutputType = resultType;
}
else if (resultType.isStderr()) {
myCurrentStderrOutputType = resultType;
}
}
pos = escSeqEndInd + 1;
}
@@ -93,10 +101,10 @@ public class AnsiEscapeDecoder {
private void updateUnhandledSuffix(@NotNull String text, @NotNull Key outputType, int unhandledSuffixLength) {
String unhandledSuffix = unhandledSuffixLength > 0 ? text.substring(text.length() - unhandledSuffixLength) : null;
if (outputType == ProcessOutputTypes.STDOUT) {
if (ProcessOutputType.isStdout(outputType)) {
myUnhandledStdout = unhandledSuffix;
}
else if (outputType == ProcessOutputTypes.STDERR) {
else if (ProcessOutputType.isStderr(outputType)) {
myUnhandledStderr = unhandledSuffix;
}
}
@@ -104,11 +112,11 @@ public class AnsiEscapeDecoder {
@NotNull
private String prependUnhandledText(@NotNull String text, @NotNull Key outputType) {
String prevUnhandledText = null;
if (outputType == ProcessOutputTypes.STDOUT) {
if (ProcessOutputType.isStdout(outputType)) {
prevUnhandledText = myUnhandledStdout;
myUnhandledStdout = null;
}
else if (outputType == ProcessOutputTypes.STDERR) {
else if (ProcessOutputType.isStderr(outputType)) {
prevUnhandledText = myUnhandledStderr;
myUnhandledStderr = null;
}
@@ -263,10 +271,13 @@ public class AnsiEscapeDecoder {
@NotNull
protected Key getCurrentOutputAttributes(@NotNull Key outputType) {
if (outputType == ProcessOutputTypes.STDERR || outputType == ProcessOutputTypes.SYSTEM) {
return outputType;
if (ProcessOutputType.isStdout(outputType)) {
return ObjectUtils.notNull(myCurrentStdoutOutputType, outputType);
}
return myCurrentTextAttributes != null ? myCurrentTextAttributes : outputType;
if (ProcessOutputType.isStderr(outputType)) {
return ObjectUtils.notNull(myCurrentStderrOutputType, outputType);
}
return outputType;
}
public interface ColoredChunksAcceptor extends ColoredTextAcceptor {
@@ -40,7 +40,8 @@ public class ColoredOutputTypeRegistry {
return ServiceManager.getService(ColoredOutputTypeRegistry.class);
}
private final Map<String, Key> myRegisteredKeys = ContainerUtil.newConcurrentMap();
private final Map<String, ProcessOutputType> myStdoutAttrsToKeyMap = ContainerUtil.newConcurrentMap();
private final Map<String, ProcessOutputType> myStderrAttrsToKeyMap = ContainerUtil.newConcurrentMap();
private static final TextAttributesKey[] myAnsiColorKeys = new TextAttributesKey[]{
ConsoleHighlighter.BLACK,
@@ -91,10 +92,12 @@ public class ColoredOutputTypeRegistry {
see full doc at http://en.wikipedia.org/wiki/ANSI_escape_code
*/
@NotNull
public Key getOutputKey(@NonNls String attribute) {
final Key key = myRegisteredKeys.get(attribute);
public ProcessOutputType getOutputType(@NonNls String attribute, @NotNull Key streamType) {
ProcessOutputType streamOutputType = streamType instanceof ProcessOutputType ? (ProcessOutputType)streamType
: (ProcessOutputType)ProcessOutputTypes.STDOUT;
Map<String, ProcessOutputType> attrsToKeyMap = ProcessOutputType.isStdout(streamType) ? myStdoutAttrsToKeyMap : myStderrAttrsToKeyMap;
ProcessOutputType key = attrsToKeyMap.get(attribute);
if (key != null) {
return key;
}
@@ -107,15 +110,23 @@ public class ColoredOutputTypeRegistry {
}
attribute = StringUtil.trimEnd(attribute, "m");
if (attribute.equals("0")) {
return ProcessOutputTypes.STDOUT;
return streamOutputType;
}
Key newKey = new Key(completeAttribute);
ProcessOutputType newKey = new ProcessOutputType(completeAttribute, streamOutputType);
AnsiConsoleViewContentType contentType = createAnsiConsoleViewContentType(attribute);
ConsoleViewContentType.registerNewConsoleViewType(newKey, contentType);
myRegisteredKeys.put(completeAttribute, newKey);
attrsToKeyMap.put(completeAttribute, newKey);
return newKey;
}
/**
* @deprecated use {@link #getOutputType(String, Key)} instead
*/
@NotNull
public Key getOutputKey(@NonNls String attribute) {
return getOutputType(attribute, ProcessOutputTypes.STDOUT);
}
private static Color getAnsiColor(final int value) {
return getColorByKey(getAnsiColorKey(value));
}
@@ -1,5 +1,6 @@
package com.intellij.execution.process;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.Pair;
import com.intellij.testFramework.PlatformTestCase;
import com.intellij.testFramework.PlatformTestUtil;
@@ -23,26 +24,27 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
private static final String STDERR_KEY = ProcessOutputTypes.STDERR.toString();
public void testTextWithoutColors() {
check(new ColoredText(""));
check(new ColoredText("simple text").addExpected("simple text", STDOUT_KEY));
check(new ColoredText("", ProcessOutputTypes.STDOUT));
check(new ColoredText("simple text", ProcessOutputTypes.STDOUT).addExpected("simple text", STDOUT_KEY));
}
public void testSingleColoredChunk() {
check(new ColoredText("Chrome 35.0.1916 (Linux): Executed 0 of 1\u001B[32m SUCCESS\u001B[39m (0 secs / 0 secs)\n")
check(new ColoredText("Chrome 35.0.1916 (Linux): Executed 0 of 1\u001B[32m SUCCESS\u001B[39m (0 secs / 0 secs)\n",
ProcessOutputTypes.STDOUT)
.addExpected("Chrome 35.0.1916 (Linux): Executed 0 of 1", STDOUT_KEY)
.addExpected(" SUCCESS", "\u001B[32m")
.addExpected(" (0 secs / 0 secs)\n", "\u001B[39m"));
}
public void testCompoundEscSeq() {
check(new ColoredText("E\u001B[41m\u001B[37mE\u001B[0mE")
check(new ColoredText("E\u001B[41m\u001B[37mE\u001B[0mE", ProcessOutputTypes.STDOUT)
.addExpected("E", STDOUT_KEY)
.addExpected("E", "\u001B[41;37m")
.addExpected("E", STDOUT_KEY));
}
public void testOtherEscSeq() {
check(new ColoredText("Plain\u001B[32mGreen\u001B[39mNormal\u001B[1A\u001B[2K\u001B[31mRed\u001B[39m")
check(new ColoredText("Plain\u001B[32mGreen\u001B[39mNormal\u001B[1A\u001B[2K\u001B[31mRed\u001B[39m", ProcessOutputTypes.STDOUT)
.addExpected("Plain", STDOUT_KEY)
.addExpected("Green", "\u001B[32m")
.addExpected("Normal", "\u001B[39m")
@@ -51,27 +53,29 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
public void testBackspaceControlSequence() {
check(false, ContainerUtil.newArrayList(
new ColoredText(" 10% 0/1 build modules\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 70% 1/1 build modules")
new ColoredText(" 10% 0/1 build modules\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 70% 1/1 build modules", ProcessOutputTypes.STDOUT)
.addExpected(" 70% 1/1 build modules", STDOUT_KEY),
new ColoredText(
"\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 40% 1/2 build modules\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 30% 1/3 build modules\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 25% 1/4 build modules")
"\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 40% 1/2 build modules\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 30% 1/3 build modules\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b\b 25% 1/4 build modules",
ProcessOutputTypes.STDOUT)
.addExpected("\n 25% 1/4 build modules", STDOUT_KEY)
));
}
public void testIncompleteEscapeSequences() {
check(true, ContainerUtil.newArrayList(
new ColoredText("\u001B"),
new ColoredText("[33m Hello\u001B[3").addExpected(" Hello", "\u001B[33m"),
new ColoredText("4m, Work!").addExpected(", Work!", "\u001B[34m")
new ColoredText("\u001B", ProcessOutputTypes.STDOUT),
new ColoredText("[33m Hello\u001B[3", ProcessOutputTypes.STDOUT).addExpected(" Hello", "\u001B[33m"),
new ColoredText("4m, Work!", ProcessOutputTypes.STDOUT).addExpected(", Work!", "\u001B[34m")
));
check(true, ContainerUtil.newArrayList(
new ColoredText("\u001B[1;33m<\u001B[34mnamespace\u001B[1")
new ColoredText("\u001B[1;33m<\u001B[34mnamespace\u001B[1", ProcessOutputTypes.STDOUT)
.addExpected("<", "\u001B[1;33m")
.addExpected("namespace", "\u001B[34m"),
new ColoredText(
";33m:abcd\u001B[0m\u001B[1;33m>\u001B[0m0\u001B[1;33m</\u001B[34mnamespace\u001B[1;33m:abcd\u001B[0m\u001B[1;33m>\u001B[0m")
";33m:abcd\u001B[0m\u001B[1;33m>\u001B[0m0\u001B[1;33m</\u001B[34mnamespace\u001B[1;33m:abcd\u001B[0m\u001B[1;33m>\u001B[0m",
ProcessOutputTypes.STDOUT)
.addExpected(":abcd", "\u001B[1;33m")
.addExpected(">", "\u001B[0;1;33m")
.addExpected("0", "stdout")
@@ -82,6 +86,17 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
));
}
public void testStderr() {
check(true, ContainerUtil.newArrayList(
new ColoredText("\u001B[33m Hello,", ProcessOutputTypes.STDOUT).addExpected(" Hello,", "\u001B[33m"),
new ColoredText("World!\n", ProcessOutputTypes.STDERR).addExpected("World!\n", STDERR_KEY),
new ColoredText("\u001B[41m Changed stderr background", ProcessOutputTypes.STDERR)
.addExpected(" Changed stderr background", "\u001B[41m"),
new ColoredText("Unchanged stdout background", ProcessOutputTypes.STDOUT)
.addExpected("Unchanged stdout background", "\u001B[33m")
));
}
private static void check(@NotNull ColoredText text) {
check(true, Collections.singletonList(text));
}
@@ -93,9 +108,8 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
AnsiEscapeDecoder.ColoredTextAcceptor acceptor = (text, attributes) -> {
actualColoredChunks.add(Pair.create(text, attributes.toString()));
};
// test stdout
for (ColoredText text : texts) {
decoder.escapeText(text.myRawText, ProcessOutputTypes.STDOUT, acceptor);
decoder.escapeText(text.myRawText, text.myOutputType, acceptor);
}
List<Pair<String, String>> expectedColoredChunks = new ArrayList<>();
for (ColoredText text : texts) {
@@ -109,7 +123,7 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
decoder = new AnsiEscapeDecoder();
for (ColoredText text : texts) {
for (int i = 0; i < text.myRawText.length(); i++) {
decoder.escapeText(String.valueOf(text.myRawText.charAt(i)), ProcessOutputTypes.STDOUT, acceptor);
decoder.escapeText(String.valueOf(text.myRawText.charAt(i)), text.myOutputType, acceptor);
}
}
expectedColoredChunks.clear();
@@ -123,20 +137,6 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
}
Assert.assertEquals(expectedColoredChunks, actualColoredChunks);
}
// test stderr
actualColoredChunks.clear();
decoder = new AnsiEscapeDecoder();
for (ColoredText text : texts) {
decoder.escapeText(text.myRawText, ProcessOutputTypes.STDERR, acceptor);
}
expectedColoredChunks.clear();
for (ColoredText text : texts) {
for (Pair<String, String> chunk : text.myExpectedColoredChunks) {
expectedColoredChunks.add(Pair.create(chunk.first, STDERR_KEY));
}
}
Assert.assertEquals(expectedColoredChunks, actualColoredChunks);
}
@NotNull
@@ -213,9 +213,11 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
private static class ColoredText {
private final String myRawText;
private final List<Pair<String, String>> myExpectedColoredChunks = new ArrayList<>();
private final Key myOutputType;
public ColoredText(@NotNull String rawText) {
public ColoredText(@NotNull String rawText, @NotNull Key outputType) {
myRawText = rawText;
myOutputType = outputType;
}
private ColoredText addExpected(@NotNull String text, @NotNull String colorKey) {
@@ -0,0 +1,80 @@
/*
* Copyright 2000-2017 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.process;
import com.intellij.openapi.util.Key;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
/**
* Represents a stream (stdout/stderr) output type. It can be a base output type or colored output type.
* Base stdout/stderr output types is constants: {@link ProcessOutputTypes#STDOUT} and
* {@link ProcessOutputTypes#STDERR}.<br/>
* A colored stdout/stderr output type is created one per each unique color attributes info decoded as
* {@code name} parameter, and base stream output type (stdout/stderr) - {@code streamType} parameter.
* <p/>
* Use {@link com.intellij.execution.ui.ConsoleViewContentType#getConsoleViewType} to get TextAttributes for an instance
* of this class.
* <p/>
* @see {@link com.intellij.execution.process.ColoredOutputTypeRegistry}
* @see {@link com.intellij.execution.ui.ConsoleViewContentType#registerNewConsoleViewType}
*/
@SuppressWarnings({"JavaDoc", "JavadocReference"})
public class ProcessOutputType extends Key {
private final ProcessOutputType myStreamType;
public ProcessOutputType(@NotNull String name, @NotNull ProcessOutputType streamType) {
super(name);
myStreamType = streamType;
}
ProcessOutputType(@NotNull String name) {
super(name);
myStreamType = null;
}
@NotNull
private ProcessOutputType getStreamType() {
return myStreamType != null ? myStreamType : this;
}
public boolean isStdout() {
return getStreamType() == ProcessOutputTypes.STDOUT;
}
public boolean isStderr() {
return getStreamType() == ProcessOutputTypes.STDERR;
}
public static boolean isStderr(@NotNull Key key) {
if (key instanceof ProcessOutputType) {
return ((ProcessOutputType)key).isStderr();
}
return false;
}
public static boolean isStdout(@NotNull Key key) {
if (key instanceof ProcessOutputType) {
return ((ProcessOutputType)key).isStdout();
}
return false;
}
@Nullable
public static ProcessOutputType tryCast(@NotNull Key key) {
return key instanceof ProcessOutputType ? (ProcessOutputType)key : null;
}
}
@@ -1,5 +1,5 @@
/*
* Copyright 2000-2009 JetBrains s.r.o.
* Copyright 2000-2017 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.
@@ -18,7 +18,23 @@ package com.intellij.execution.process;
import com.intellij.openapi.util.Key;
public interface ProcessOutputTypes {
Key SYSTEM = new Key("system");
Key STDOUT = new Key("stdout");
Key STDERR = new Key("stderr");
Key SYSTEM = new ProcessOutputType("system");
/**
* Represents process standard output stream.<p>
* Please note that stdout colored output type doesn't equal to this instance: use
* <pre>{@code ProcessOutputType.isStdout(key)}</pre>
* instead of
* <pre>{@code ProcessOutputTypes.STDOUT.equals(key)} or ProcessOutputTypes.STDOUT == key</pre>
*/
Key STDOUT = new ProcessOutputType("stdout");
/**
* Represents process standard error stream.<p>
* Please note that stderr colored output type doesn't equal to this instance: use
* <pre>{@code ProcessOutputType.isStderr(key)}</pre>
* instead of
* <pre>{@code ProcessOutputTypes.STDERR.equals(key) or ProcessOutputTypes.STDERR == key}</pre>
*/
Key STDERR = new ProcessOutputType("stderr");
}