WEB-23055 Support ASCII control characters to improve webpack progress showing in...
authorSergey Simonchik <sergey.simonchik@jetbrains.com>
Wed, 31 Aug 2016 18:33:12 +0000 (21:33 +0300)
committerSergey Simonchik <sergey.simonchik@jetbrains.com>
Wed, 31 Aug 2016 18:34:41 +0000 (21:34 +0300)
platform/platform-api/src/com/intellij/execution/process/AnsiEscapeDecoder.java
platform/platform-tests/testSrc/com/intellij/execution/process/AnsiEscapeDecoderTest.java

index 67c4af413e74c1cc1f24d4fabd14af928ac3b17c..d6d288023625c639e8cbc3f934c6dcb93524a3f1 100644 (file)
@@ -18,6 +18,7 @@ package com.intellij.execution.process;
 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.containers.ContainerUtil;
 import org.jetbrains.annotations.NotNull;
 import org.jetbrains.annotations.Nullable;
@@ -34,6 +35,7 @@ public class AnsiEscapeDecoder {
   private static final char ESC_CHAR = '\u001B'; // Escape sequence start character
   private static final String CSI = ESC_CHAR + "["; // "Control Sequence Initiator"
   private static final Pattern INNER_PATTERN = Pattern.compile(Pattern.quote("m" + CSI));
+  private static final char BACKSPACE = '\b';
 
   private Key myCurrentTextAttributes;
 
@@ -48,6 +50,7 @@ public class AnsiEscapeDecoder {
   public void escapeText(@NotNull String text, @NotNull Key outputType, @NotNull ColoredTextAcceptor textAcceptor) {
     List<Pair<String, Key>> chunks = null;
     int pos = 0;
+    text = normalizeAsciiControlCharacters(text);
     while (true) {
       int escSeqBeginInd = text.indexOf(CSI, pos);
       if (escSeqBeginInd < 0) {
@@ -77,6 +80,48 @@ public class AnsiEscapeDecoder {
     }
   }
 
+  @NotNull
+  private static String normalizeAsciiControlCharacters(@NotNull String text) {
+    int ind = text.indexOf(BACKSPACE);
+    if (ind == -1) {
+      return text;
+    }
+    StringBuilder result = new StringBuilder();
+    int i = 0;
+    int guardIndex = 0;
+    boolean removalFromPrevTextAttempted = false;
+    while (i < text.length()) {
+      LineSeparator lineSeparator = StringUtil.findStartingLineSeparator(text, i);
+      if (lineSeparator != null) {
+        i += lineSeparator.getSeparatorString().length();
+        result.append(lineSeparator.getSeparatorString());
+        guardIndex = result.length();
+      }
+      else {
+        if (text.charAt(i) == BACKSPACE) {
+          if (result.length() > guardIndex) {
+            result.setLength(result.length() - 1);
+          }
+          else if (guardIndex == 0) {
+            removalFromPrevTextAttempted = true;
+          }
+        }
+        else {
+          result.append(text.charAt(i));
+        }
+        i++;
+      }
+    }
+    if (removalFromPrevTextAttempted) {
+      // This workaround allows to pretty print progress splitting it into several lines:
+      //  25% 1/4 build modules
+      //  40% 2/4 build modules
+      // instead of one single line "25% 1/4 build modules 40% 2/4 build modules"
+      result.insert(0, LineSeparator.LF.getSeparatorString());
+    }
+    return result.toString();
+  }
+
   /*
    * Selects all consecutive escape sequences and returns escape sequence end index (exclusive).
    * If the escape sequence isn't finished, returns -1.
index 519837cf7aba72dcb5fbeef4d76a3fc5a97a2ebf..7853654c745859437bf52a8b861c5483462230b6 100644 (file)
@@ -3,7 +3,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.util.Function;
 import com.intellij.util.containers.ContainerUtil;
 import org.jetbrains.annotations.NotNull;
 import org.junit.Assert;
@@ -55,6 +54,22 @@ public class AnsiEscapeDecoderTest extends PlatformTestCase {
     );
   }
 
+  public void testBackspaceControlSequence() throws Exception {
+    AnsiEscapeDecoder decoder = new AnsiEscapeDecoder();
+    decoder.escapeText(" 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.STDERR,
+                       createExpectedAcceptor(
+                         Pair.create(" 70% 1/1 build modules", ProcessOutputTypes.STDERR)
+                       )
+    );
+    decoder.escapeText("\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.STDERR,
+                       createExpectedAcceptor(
+                         Pair.create("\n 25% 1/4 build modules", ProcessOutputTypes.STDERR)
+                       )
+    );
+  }
+
   @NotNull
   private static List<Pair<String, String>> toListWithKeyName(@NotNull Collection<Pair<String, Key>> list) {
     return ContainerUtil.map(list, pair -> Pair.create(pair.first, pair.second.toString()));