-
Notifications
You must be signed in to change notification settings - Fork 352
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add helper class to manage streaming process output.
Previously, we had fairly complex code for buffering and chunking process output into individual logging statements. This commit moves that logic into a separate class that can be tested in isolation to ensure that we correctly preserve all relevant output from the process.
- Loading branch information
Showing
3 changed files
with
139 additions
and
38 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
74 changes: 74 additions & 0 deletions
74
metals/src/main/scala/scala/meta/internal/metals/ProcessOutput.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
package scala.meta.internal.metals | ||
|
||
import java.lang.StringBuilder | ||
import java.nio.ByteBuffer | ||
import java.nio.charset.StandardCharsets | ||
import fansi.ErrorMode | ||
|
||
/** | ||
* Handles streaming console output from a system process with potential ANSI codes. | ||
* | ||
* @param onLine The callback handler when the process has printed a single line. | ||
* Guaranteed to have no newline \n characters. | ||
*/ | ||
class ProcessOutput(onLine: String => Unit) { | ||
private var buffer = new StringBuilder() | ||
|
||
/** The process has exited, clears out buffered output. */ | ||
def onProcessExit(): Unit = { | ||
if (buffer.length() > 0) { | ||
onLine(buffer.toString()) | ||
reset() | ||
} | ||
} | ||
|
||
def onByteOutput(bytes: ByteBuffer): this.type = { | ||
onPlainOutput(toPlainString(bytes)) | ||
} | ||
|
||
def onStringOutput(text: String): this.type = { | ||
onPlainOutput(toPlainString(text)) | ||
} | ||
|
||
private def onPlainOutput(text: String): this.type = { | ||
def loop(start: Int): Unit = { | ||
val newline = text.indexOf('\n', start) | ||
if (newline < 0) { | ||
buffer.append(text, start, text.length()) | ||
} else { | ||
onLine(lineSubstring(text, start, newline)) | ||
loop(newline + 1) | ||
} | ||
} | ||
loop(0) | ||
this | ||
} | ||
|
||
private def lineSubstring(text: String, from: Int, to: Int): String = { | ||
val end = | ||
// Convert \r\n line breaks into \n line breaks. | ||
if (to > 0 && text.charAt(to - 1) == '\r') to - 1 | ||
else to | ||
if (buffer.length() == 0) text.substring(from, end) | ||
else { | ||
val line = buffer.append(text, from, to).toString() | ||
reset() | ||
line | ||
} | ||
} | ||
|
||
private def reset(): Unit = ( | ||
buffer = new StringBuilder() | ||
) | ||
|
||
private def toPlainString(buffer: ByteBuffer): String = { | ||
val bytes = new Array[Byte](buffer.remaining()) | ||
buffer.get(bytes) | ||
val ansiString = new String(bytes, StandardCharsets.UTF_8) | ||
toPlainString(ansiString) | ||
} | ||
|
||
private def toPlainString(ansiString: String): String = { | ||
fansi.Str(ansiString, ErrorMode.Sanitize).plainText | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
package tests | ||
|
||
import scala.meta.internal.metals.ProcessOutput | ||
import scala.collection.mutable | ||
|
||
object ProcessOutputSuite extends BaseSuite { | ||
def check( | ||
name: String, | ||
fn: ProcessOutput => Unit, | ||
expected: List[String] | ||
): Unit = { | ||
test(name) { | ||
var buf = mutable.ListBuffer.empty[String] | ||
val output = new ProcessOutput(line => buf += line) | ||
fn(output) | ||
output.onProcessExit() | ||
assertEquals(buf.toList, expected) | ||
} | ||
} | ||
|
||
check( | ||
"flush", | ||
_.onStringOutput("a") | ||
.onStringOutput("b"), | ||
List("ab") | ||
) | ||
|
||
check( | ||
"loop", | ||
_.onStringOutput("a\nb"), | ||
List("a", "b") | ||
) | ||
|
||
check( | ||
"eol", | ||
_.onStringOutput("a\n"), | ||
List("a") | ||
) | ||
|
||
check( | ||
"eol2", | ||
_.onStringOutput("a\n\n"), | ||
List("a", "") | ||
) | ||
|
||
check( | ||
"ansi", | ||
_.onStringOutput(fansi.Color.Blue("blue").toString()), | ||
List("blue") | ||
) | ||
|
||
check( | ||
"windows", | ||
_.onStringOutput("a\r\nb"), | ||
List("a", "b") | ||
) | ||
} |