diff --git a/platform/platform-api/src/com/intellij/execution/process/AnsiEscapeDecoder.java b/platform/platform-api/src/com/intellij/execution/process/AnsiEscapeDecoder.java index 30e8d77f6766..38f2aea647e9 100644 --- a/platform/platform-api/src/com/intellij/execution/process/AnsiEscapeDecoder.java +++ b/platform/platform-api/src/com/intellij/execution/process/AnsiEscapeDecoder.java @@ -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 { diff --git a/platform/platform-api/src/com/intellij/execution/process/ColoredOutputTypeRegistry.java b/platform/platform-api/src/com/intellij/execution/process/ColoredOutputTypeRegistry.java index 5110c4218659..580ad945abc0 100644 --- a/platform/platform-api/src/com/intellij/execution/process/ColoredOutputTypeRegistry.java +++ b/platform/platform-api/src/com/intellij/execution/process/ColoredOutputTypeRegistry.java @@ -40,7 +40,8 @@ public class ColoredOutputTypeRegistry { return ServiceManager.getService(ColoredOutputTypeRegistry.class); } - private final Map myRegisteredKeys = ContainerUtil.newConcurrentMap(); + private final Map myStdoutAttrsToKeyMap = ContainerUtil.newConcurrentMap(); + private final Map 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 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)); } diff --git a/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java b/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java index b9107fca9807..47d1fb9dcc7c 100644 --- a/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java +++ b/platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java @@ -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[0m") + ";33m:abcd\u001B[0m\u001B[1;33m>\u001B[0m0\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> 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 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> 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) { diff --git a/platform/util/src/com/intellij/execution/process/ProcessOutputType.java b/platform/util/src/com/intellij/execution/process/ProcessOutputType.java new file mode 100644 index 000000000000..773a75fb01ed --- /dev/null +++ b/platform/util/src/com/intellij/execution/process/ProcessOutputType.java @@ -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}.
+ * 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. + *

+ * Use {@link com.intellij.execution.ui.ConsoleViewContentType#getConsoleViewType} to get TextAttributes for an instance + * of this class. + *

+ * @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; + } +} diff --git a/platform/util/src/com/intellij/execution/process/ProcessOutputTypes.java b/platform/util/src/com/intellij/execution/process/ProcessOutputTypes.java index a723f55ccc26..17d5ef6ca097 100644 --- a/platform/util/src/com/intellij/execution/process/ProcessOutputTypes.java +++ b/platform/util/src/com/intellij/execution/process/ProcessOutputTypes.java @@ -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.

+ * Please note that stdout colored output type doesn't equal to this instance: use + *

{@code ProcessOutputType.isStdout(key)}
+ * instead of + *
{@code ProcessOutputTypes.STDOUT.equals(key)} or ProcessOutputTypes.STDOUT == key
+ */ + Key STDOUT = new ProcessOutputType("stdout"); + + /** + * Represents process standard error stream.

+ * Please note that stderr colored output type doesn't equal to this instance: use + *

{@code ProcessOutputType.isStderr(key)}
+ * instead of + *
{@code ProcessOutputTypes.STDERR.equals(key) or ProcessOutputTypes.STDERR == key}
+ */ + Key STDERR = new ProcessOutputType("stderr"); }