mirror of https://github.com/sbt/sbt.git
Add additional ansi-escape removal functions
This commit adds a number of functions for stripping ansi escape characters and/or finding the position of the cursor in a line that may contain colors and moves. The motivation for EscHelpers.cursorPosition is that when printing progress lines, we need to know the visual dimensions of the last line printed to the prompt. The EscHelpers.stripColorsAndMoves can be used to remove all ansi escape sequences. Finally EscHelpers.stripMoves leaves colors but strips out all other escape sequences. This is so we can reprint the terminal prompt during supershell. If we didn't strip out the escape sequences, we could inadvertently blow away everything below the cursor in cases where we actually want the lines below the cursor to persist.
This commit is contained in:
parent
13138457cf
commit
034b9690c1
|
|
@ -7,6 +7,9 @@
|
|||
|
||||
package sbt.internal.util
|
||||
|
||||
import scala.collection.mutable.ArrayBuffer
|
||||
import scala.util.Try
|
||||
|
||||
object EscHelpers {
|
||||
|
||||
/** Escape character, used to introduce an escape sequence. */
|
||||
|
|
@ -84,6 +87,110 @@ object EscHelpers {
|
|||
nextESC(s, next, sb)
|
||||
}
|
||||
}
|
||||
private[this] val esc = 1
|
||||
private[this] val csi = 2
|
||||
def cursorPosition(s: String): Int = {
|
||||
val bytes = s.getBytes
|
||||
var i = 0
|
||||
var index = 0
|
||||
var state = 0
|
||||
val digit = new ArrayBuffer[Byte]
|
||||
var leftDigit = -1
|
||||
while (i < bytes.length) {
|
||||
bytes(i) match {
|
||||
case 27 => state = esc
|
||||
case b if (state == esc || state == csi) && b >= 48 && b < 58 =>
|
||||
state = csi
|
||||
digit += b
|
||||
case '[' if state == esc => state = csi
|
||||
case 8 =>
|
||||
state = 0
|
||||
index = index - 1
|
||||
case b if state == csi =>
|
||||
leftDigit = Try(new String(digit.toArray).toInt).getOrElse(0)
|
||||
state = 0
|
||||
b.toChar match {
|
||||
case 'D' => index = math.max(index - leftDigit, 0)
|
||||
case 'C' => index += leftDigit
|
||||
case 'K' =>
|
||||
case 'J' => if (leftDigit == 2) index = 0
|
||||
case 'm' =>
|
||||
case ';' => state = csi
|
||||
case _ =>
|
||||
}
|
||||
digit.clear()
|
||||
case _ =>
|
||||
index += 1
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
index
|
||||
}
|
||||
def stripMoves(s: String): String = {
|
||||
val bytes = s.getBytes
|
||||
val res = Array.fill[Byte](bytes.length)(0)
|
||||
var index = 0
|
||||
var lastEscapeIndex = -1
|
||||
var state = 0
|
||||
def set(b: Byte) = {
|
||||
res(index) = b
|
||||
index += 1
|
||||
}
|
||||
bytes.foreach { b =>
|
||||
set(b)
|
||||
b match {
|
||||
case 27 =>
|
||||
state = esc
|
||||
lastEscapeIndex = math.max(0, index)
|
||||
case b if b == '[' && state == esc => state = csi
|
||||
case 'm' => state = 0
|
||||
case b if state == csi && (b < 48 || b >= 58) && b != ';' =>
|
||||
state = 0
|
||||
index = math.max(0, lastEscapeIndex - 1)
|
||||
case b =>
|
||||
}
|
||||
}
|
||||
new String(res, 0, index)
|
||||
}
|
||||
def stripColorsAndMoves(s: String): String = {
|
||||
val bytes = s.getBytes
|
||||
val res = Array.fill[Byte](bytes.length)(0)
|
||||
var i = 0
|
||||
var index = 0
|
||||
var state = 0
|
||||
var limit = 0
|
||||
val digit = new ArrayBuffer[Byte]
|
||||
var leftDigit = -1
|
||||
bytes.foreach {
|
||||
case 27 => state = esc
|
||||
case b if (state == esc || state == csi) && b >= 48 && b < 58 =>
|
||||
state = csi
|
||||
digit += b
|
||||
case '[' if state == esc => state = csi
|
||||
case 8 =>
|
||||
state = 0
|
||||
index = math.max(index - 1, 0)
|
||||
case b if state == csi =>
|
||||
leftDigit = Try(new String(digit.toArray).toInt).getOrElse(0)
|
||||
state = 0
|
||||
b.toChar match {
|
||||
case 'D' => index = math.max(index - leftDigit, 0)
|
||||
case 'C' => index = math.min(limit, math.min(index + leftDigit, res.length - 1))
|
||||
case 'K' | 'J' =>
|
||||
if (leftDigit > 0) (0 until index).foreach(res(_) = 32)
|
||||
else res(index) = 32
|
||||
case 'm' =>
|
||||
case ';' => state = csi
|
||||
case _ =>
|
||||
}
|
||||
digit.clear()
|
||||
case b =>
|
||||
res(index) = b
|
||||
index += 1
|
||||
limit = math.max(limit, index)
|
||||
}
|
||||
new String(res, 0, limit)
|
||||
}
|
||||
|
||||
/** Skips the escape sequence starting at `i-1`. `i` should be positioned at the character after the ESC that starts the sequence. */
|
||||
private[this] def skipESC(s: String, i: Int): Int = {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* sbt
|
||||
* Copyright 2011 - 2018, Lightbend, Inc.
|
||||
* Copyright 2008 - 2010, Mark Harrah
|
||||
* Licensed under Apache License 2.0 (see LICENSE)
|
||||
*/
|
||||
|
||||
package sbt.internal.util
|
||||
|
||||
import org.scalatest.FlatSpec
|
||||
|
||||
class CleanStringSpec extends FlatSpec {
|
||||
"EscHelpers" should "not modify normal strings" in {
|
||||
val cleanString = s"1234"
|
||||
assert(EscHelpers.stripColorsAndMoves(cleanString) == cleanString)
|
||||
}
|
||||
it should "remove delete lines" in {
|
||||
val clean = "1234"
|
||||
val string = s"${ConsoleAppender.DeleteLine}$clean"
|
||||
assert(EscHelpers.stripColorsAndMoves(string) == clean)
|
||||
}
|
||||
it should "remove cursor left" in {
|
||||
val clean = "1234"
|
||||
val backspaced = s"1235${ConsoleAppender.cursorLeft(1)}${ConsoleAppender.clearLine(0)}4"
|
||||
assert(EscHelpers.stripColorsAndMoves(backspaced) == clean)
|
||||
}
|
||||
it should "remove colors" in {
|
||||
val clean = "1234"
|
||||
val colored = s"${scala.Console.RED}$clean${scala.Console.RESET}"
|
||||
assert(EscHelpers.stripColorsAndMoves(colored) == clean)
|
||||
}
|
||||
it should "remove backspaces" in {
|
||||
// Taken from an actual failure case. In the scala client, type 'clean', then type backspace
|
||||
// five times to clear 'clean' and then retype 'clean'.
|
||||
val bytes = Array[Byte](27, 91, 50, 75, 27, 91, 48, 74, 27, 91, 50, 75, 27, 91, 49, 48, 48, 48,
|
||||
68, 115, 98, 116, 58, 115, 99, 97, 108, 97, 45, 99, 111, 109, 112, 105, 108, 101, 27, 91, 51,
|
||||
54, 109, 62, 32, 27, 91, 48, 109, 99, 108, 101, 97, 110, 8, 27, 91, 75, 110)
|
||||
val str = new String(bytes)
|
||||
assert(EscHelpers.stripColorsAndMoves(str) == "sbt:scala-compile> clean")
|
||||
}
|
||||
it should "handle cursor left overwrite" in {
|
||||
val clean = "1234"
|
||||
val backspaced = s"1235${8.toChar}4${8.toChar}"
|
||||
assert(EscHelpers.stripColorsAndMoves(backspaced) == clean)
|
||||
}
|
||||
it should "remove moves in string with only moves" in {
|
||||
val original =
|
||||
new String(Array[Byte](27, 91, 50, 75, 27, 91, 51, 65, 27, 91, 49, 48, 48, 48, 68))
|
||||
assert(EscHelpers.stripMoves(original) == "")
|
||||
}
|
||||
it should "remove moves in string with moves and letters" in {
|
||||
val original = new String(
|
||||
Array[Byte](27, 91, 50, 75, 27, 91, 51, 65) ++ "foo".getBytes ++ Array[Byte](27, 91, 49, 48,
|
||||
48, 48, 68)
|
||||
)
|
||||
assert(EscHelpers.stripMoves(original) == "foo")
|
||||
}
|
||||
it should "preserve colors" in {
|
||||
val original = new String(
|
||||
Array[Byte](27, 91, 49, 48, 48, 48, 68, 27, 91, 48, 74, 102, 111, 111, 27, 91, 51, 54, 109,
|
||||
62, 32, 27, 91, 48, 109)
|
||||
) // this is taken from an sbt prompt that looks like "foo> " with the > rendered blue
|
||||
val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62))
|
||||
assert(EscHelpers.stripMoves(original) == "foo" + colorArrow + " " + scala.Console.RESET)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue