Skip to content

Commit

Permalink
RUN: Partial support for ANSI colors ib build toolwindow
Browse files Browse the repository at this point in the history
  • Loading branch information
mchernyavsky authored and mchernyavsky committed Aug 26, 2019
1 parent 5190523 commit d6ec3ce
Show file tree
Hide file tree
Showing 10 changed files with 114 additions and 137 deletions.
11 changes: 8 additions & 3 deletions src/main/kotlin/org/rust/cargo/runconfig/CargoRunStateBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package org.rust.cargo.runconfig

import com.intellij.execution.configurations.CommandLineState
import com.intellij.execution.configurations.PtyCommandLine
import com.intellij.execution.process.KillableProcessHandler
import com.intellij.execution.process.ProcessHandler
import com.intellij.execution.process.ProcessTerminatedListener
import com.intellij.execution.runners.ExecutionEnvironment
Expand Down Expand Up @@ -51,16 +52,20 @@ abstract class CargoRunStateBase(
return commandLine
}

override fun startProcess(): ProcessHandler = startProcess(emulateTerminal = false)
override fun startProcess(): ProcessHandler = startProcess(useColoredProcessHandler = true, emulateTerminal = false)

fun startProcess(emulateTerminal: Boolean): ProcessHandler {
fun startProcess(useColoredProcessHandler: Boolean, emulateTerminal: Boolean): ProcessHandler {
var commandLine = cargo().toColoredCommandLine(prepareCommandLine())
if (emulateTerminal) {
commandLine = PtyCommandLine(commandLine)
.withInitialColumns(PtyCommandLine.MAX_COLUMNS)
.withConsoleMode(false)
}
val handler = RsKillableColoredProcessHandler(commandLine)
val handler = if (useColoredProcessHandler) {
RsKillableColoredProcessHandler(commandLine)
} else {
KillableProcessHandler(commandLine)
}
ProcessTerminatedListener.attach(handler) // shows exit code upon termination
return handler
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class CargoTestCommandRunner : AsyncProgramRunner<RunnerSettings>() {
val buildCmd = if (cmdHasNoRun) state.commandLine else state.commandLine.prependArgument("--no-run")
val buildConfig = CargoCommandConfiguration.CleanConfiguration.Ok(buildCmd, state.config.toolchain)
val buildState = CargoRunState(state.environment, state.runConfiguration, buildConfig)
buildState.startProcess(emulateTerminal = false)
buildState.startProcess(useColoredProcessHandler = true, emulateTerminal = false)
}
val exitCode = AsyncPromise<Int?>()
RunContentExecutor(project, buildProcessHandler)
Expand Down
15 changes: 13 additions & 2 deletions src/main/kotlin/org/rust/cargo/runconfig/RsAnsiEscapeDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package org.rust.cargo.runconfig

import com.intellij.execution.process.AnsiEscapeDecoder
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.text.StringUtil
Expand All @@ -27,7 +28,9 @@ class RsAnsiEscapeDecoder : AnsiEscapeDecoder() {

companion object {
const val CSI: String = "\u001B[" // "Control Sequence Initiator"
private val ANSI_CONTROL_SEQUENCE_REGEX: Regex = """${StringUtil.escapeToRegexp(CSI)}([^m]*;[^m]*)m""".toRegex()

@JvmField
val ANSI_SGR_RE: Regex = """${StringUtil.escapeToRegexp(CSI)}(\d+(;\d+)*)m""".toRegex()

private const val ANSI_SET_FOREGROUND_ATTR: Int = 38
private const val ANSI_SET_BACKGROUND_ATTR: Int = 48
Expand All @@ -42,7 +45,7 @@ class RsAnsiEscapeDecoder : AnsiEscapeDecoder() {
* @param text a string with ANSI escape sequences
*/
private fun quantizeAnsiColors(text: String): String = text
.replace(ANSI_CONTROL_SEQUENCE_REGEX) {
.replace(ANSI_SGR_RE) {
val rawAttributes = it.destructured.component1().split(";").iterator()
val result = mutableListOf<Int>()
while (rawAttributes.hasNext()) {
Expand Down Expand Up @@ -158,3 +161,11 @@ class RsAnsiEscapeDecoder : AnsiEscapeDecoder() {
}
}
}

fun AnsiEscapeDecoder.removeEscapeSequences(text: String): String {
val chunks = mutableListOf<String>()
escapeText(text, ProcessOutputTypes.STDOUT) { chunk, _ ->
chunks.add(chunk)
}
return chunks.joinToString("")
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,31 @@ import com.intellij.build.BuildProgressListener
import com.intellij.build.DefaultBuildDescriptor
import com.intellij.build.events.impl.*
import com.intellij.build.output.BuildOutputInstantReaderImpl
import com.intellij.execution.process.AnsiEscapeDecoder
import com.intellij.execution.process.ProcessAdapter
import com.intellij.execution.process.ProcessEvent
import com.intellij.execution.process.ProcessOutputTypes
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.vfs.VfsUtil
import org.rust.cargo.CargoConstants
import org.rust.cargo.runconfig.RsAnsiEscapeDecoder
import org.rust.cargo.runconfig.RsAnsiEscapeDecoder.Companion.ANSI_SGR_RE
import org.rust.cargo.runconfig.RsAnsiEscapeDecoder.Companion.CSI
import org.rust.cargo.runconfig.RsExecutableRunner.Companion.binaries
import org.rust.cargo.runconfig.createFilters

@Suppress("UnstableApiUsage")
class CargoBuildAdapter(
private val context: CargoBuildContext,
private val buildProgressListener: BuildProgressListener
) : ProcessAdapter(), AnsiEscapeDecoder.ColoredTextAcceptor {
private val decoder: AnsiEscapeDecoder = RsAnsiEscapeDecoder()

) : ProcessAdapter() {
private val textBuffer: MutableList<String> = mutableListOf()
private val buildOutputParser: CargoBuildEventsConverter = CargoBuildEventsConverter(context)

private val instantReader = BuildOutputInstantReaderImpl(
context.buildId,
context.buildId,
buildProgressListener,
listOf(buildOutputParser)
)

private val textBuffer: MutableList<String> = mutableListOf()

init {
val descriptor = DefaultBuildDescriptor(
context.buildId,
Expand Down Expand Up @@ -79,38 +74,17 @@ class CargoBuildAdapter(
}

override fun onTextAvailable(event: ProcessEvent, outputType: Key<*>) {
var rawText = event.text
if (SystemInfo.isWindows && rawText.matches(BUILD_PROGRESS_INNER_RE)) {
rawText += "\n"
}

textBuffer.add(rawText)
if (!rawText.endsWith("\n")) return

val concat = textBuffer.joinToString("")

// If the line contains a JSON message (contains `{"reason"` substring), then it should end with `}\n`,
// otherwise the line contains only part of the message.
if (concat.contains("{\"reason\"") && !concat.endsWith("}\n")) return

val text = concat.replace(BUILD_PROGRESS_FULL_RE) { it.value.trimEnd(' ', '\r', '\n') + "\n" }
textBuffer.clear()

decoder.escapeText(text, outputType, this)
buildOutputParser.parseOutput(
text.replace(BUILD_PROGRESS_FULL_RE, ""),
outputType == ProcessOutputTypes.STDOUT
) {
buildProgressListener.onEvent(context.buildId, it)
}
}

override fun coloredTextAvailable(text: String, outputType: Key<*>) {
textBuffer.add(event.text)
if (!event.text.endsWith("\n")) return
val text = textBuffer.joinToString("")
.replace(ERASE_LINES_RE, "\n")
.replace(BUILD_PROGRESS_RE) { it.value.trimEnd(' ', '\r', '\n') + "\n" }
instantReader.append(text)
textBuffer.clear()
}

companion object {
private val BUILD_PROGRESS_INNER_RE: Regex = """ \[ *=*>? *] \d+/\d+: [\w\-(.)]+(, [\w\-(.)]+)*""".toRegex()
private val BUILD_PROGRESS_FULL_RE: Regex = """ *Building$BUILD_PROGRESS_INNER_RE( *[\r\n])*""".toRegex()
private val ERASE_LINES_RE: Regex = """${StringUtil.escapeToRegexp(CSI)}\d?K""".toRegex()
private val BUILD_PROGRESS_RE: Regex = """($ANSI_SGR_RE)* *Building($ANSI_SGR_RE)* \[ *=*>? *] \d+/\d+: [\w\-(.)]+(, [\w\-(.)]+)*( *[\r\n])*""".toRegex()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,32 @@
package org.rust.cargo.runconfig.buildtool

import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.JsonSyntaxException
import com.intellij.build.FilePosition
import com.intellij.build.events.BuildEvent
import com.intellij.build.events.MessageEvent
import com.intellij.build.events.StartEvent
import com.intellij.build.events.impl.*
import com.intellij.build.output.BuildOutputInstantReader
import com.intellij.build.output.BuildOutputParser
import com.intellij.execution.process.AnsiEscapeDecoder
import com.intellij.openapi.progress.ProgressIndicator
import org.rust.cargo.runconfig.removeEscapeSequences
import org.rust.cargo.toolchain.CargoTopMessage
import org.rust.cargo.toolchain.RustcSpan
import org.rust.cargo.toolchain.impl.CargoMetadata
import org.rust.ide.annotator.isValid
import org.rust.stdext.JsonUtil.tryParseJsonObject
import java.nio.file.Path
import java.nio.file.Paths
import java.util.function.Consumer

@Suppress("UnstableApiUsage")
class CargoBuildEventsConverter(private val context: CargoBuildContext) : BuildOutputParser {
private val decoder: AnsiEscapeDecoder = AnsiEscapeDecoder()

private val startEvents: MutableList<StartEvent> = mutableListOf()
private val messageEvents: MutableSet<MessageEvent> = hashSetOf()

private var jsonBuffer: String = ""

private val rawBinaries: MutableSet<String> = hashSetOf()
val binaries: List<Path> get() = rawBinaries.map { Paths.get(it) }

Expand All @@ -39,49 +40,27 @@ class CargoBuildEventsConverter(private val context: CargoBuildContext) : BuildO
reader: BuildOutputInstantReader,
messageConsumer: Consumer<in BuildEvent>
): Boolean {
val text = jsonBuffer + line
if (text.startsWith("{")) {
if (text.endsWith("}")) {
jsonBuffer = ""
} else {
jsonBuffer = text + "\n"
return true
}
}

return try {
val json = parseJsonObject(text)
tryHandleRustcMessage(json, messageConsumer) || tryHandleRustcArtifact(json)
} catch (e: JsonSyntaxException) {
tryHandleCargoMessage(text, messageConsumer)
}
}

fun parseOutput(
line: String,
stdOut: Boolean,
messageConsumer: (BuildEvent) -> Unit
): Boolean {
val message = try {
val json = parseJsonObject(line)
val rustcMessage = json?.let { CargoTopMessage.fromJson(it)?.message }
rustcMessage?.rendered ?: return false
} catch (e: JsonSyntaxException) {
line
val cleanLine = if ('\r' in line) line.substringAfterLast('\r') else line
val jsonObject = tryParseJsonObject(cleanLine.dropWhile { it != '{' })
?: return tryHandleCargoMessage(cleanLine, messageConsumer)
val prefix = cleanLine.substringBefore('{')
if (prefix.isNotEmpty()) {
messageConsumer.acceptText(prefix)
}
val formattedMessage = if (message.endsWith('\n')) message else message + '\n'
val event = OutputBuildEventImpl(context.buildId, formattedMessage, stdOut)
messageConsumer(event)
return true
return tryHandleRustcMessage(jsonObject, messageConsumer) || tryHandleRustcArtifact(jsonObject)
}

private fun tryHandleRustcMessage(json: JsonObject?, messageConsumer: Consumer<in BuildEvent>): Boolean {
val topMessage = json?.let { CargoTopMessage.fromJson(it) } ?: return false
private fun tryHandleRustcMessage(jsonObject: JsonObject, messageConsumer: Consumer<in BuildEvent>): Boolean {
val topMessage = CargoTopMessage.fromJson(jsonObject) ?: return false
val rustcMessage = topMessage.message

val detailedMessage = rustcMessage.rendered
if (detailedMessage != null) {
messageConsumer.acceptText(detailedMessage.withNewLine())
}

val message = rustcMessage.message.trim().capitalize().trimEnd('.')
if (message.startsWith("Aborting due")) return true
val detailedMessage = rustcMessage.rendered

val parentEventId = topMessage.package_id.substringBefore("(").trimEnd()

Expand All @@ -103,8 +82,8 @@ class CargoBuildEventsConverter(private val context: CargoBuildContext) : BuildO
return true
}

private fun tryHandleRustcArtifact(json: JsonObject?): Boolean {
val rustcArtifact = json?.let { CargoMetadata.Artifact.fromJson(it) } ?: return false
private fun tryHandleRustcArtifact(jsonObject: JsonObject): Boolean {
val rustcArtifact = CargoMetadata.Artifact.fromJson(jsonObject) ?: return false

val isSuitableTarget = when (rustcArtifact.target.cleanKind) {
CargoMetadata.TargetKind.BIN -> true
Expand All @@ -130,29 +109,37 @@ class CargoBuildEventsConverter(private val context: CargoBuildContext) : BuildO
}

private fun tryHandleCargoMessage(line: String, messageConsumer: Consumer<in BuildEvent>): Boolean {
val kind = getMessageKind(line.substringBefore(":"))
val message = line
val cleanLine = decoder.removeEscapeSequences(line)
if (cleanLine.isEmpty()) return true

val kind = getMessageKind(cleanLine.substringBefore(":"))
val message = cleanLine
.let { if (kind in ERROR_OR_WARNING) it.substringAfter(":") else it }
.removePrefix(" internal compiler error:")
.trim()
.capitalize()
.trimEnd('.')

when {
message.startsWith("Compiling") ->
handleCompilingMessage(message, false, messageConsumer)
message.startsWith("Fresh") ->
handleCompilingMessage(message, true, messageConsumer)
message.startsWith("Building") ->
handleProgressMessage(line, messageConsumer)
message.startsWith("Building") -> {
handleProgressMessage(cleanLine, messageConsumer)
return true // don't print progress
}
message.startsWith("Finished") ->
handleFinishedMessage(null, messageConsumer)
message.startsWith("Could not compile") -> {
val taskName = message.substringAfter("`").substringBefore("`")
handleFinishedMessage(taskName, messageConsumer)
}
kind in ERROR_OR_WARNING ->
handleProblemMessage(kind, message, line, messageConsumer)
handleProblemMessage(kind, message, cleanLine, messageConsumer)
}

messageConsumer.acceptText(line.withNewLine())
return true
}

Expand Down Expand Up @@ -280,18 +267,17 @@ class CargoBuildEventsConverter(private val context: CargoBuildContext) : BuildO
)
}

private fun Consumer<in BuildEvent>.acceptText(text: String) = accept(OutputBuildEventImpl(context.buildId, text, true))

companion object {
const val RUSTC_MESSAGE_GROUP: String = "Rust compiler"

private val PARSER: JsonParser = JsonParser()

private val PROGRESS_TOTAL_RE: Regex = """(\d+)/(\d+)""".toRegex()

private val ERROR_OR_WARNING: List<MessageEvent.Kind> =
listOf(MessageEvent.Kind.ERROR, MessageEvent.Kind.WARNING)

private fun parseJsonObject(line: String): JsonObject? =
PARSER.parse(line).takeIf { it.isJsonObject }?.asJsonObject
private fun String.withNewLine(): String = if (endsWith('\n')) this else this + '\n'

private fun getMessageKind(kind: String): MessageEvent.Kind =
when (kind) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ object CargoBuildManager {
buildToolWindow?.show(null)
}

processHandler = state.startProcess(emulateTerminal = true)
processHandler = state.startProcess(useColoredProcessHandler = false, emulateTerminal = true)
processHandler.addProcessListener(CargoBuildAdapter(this, buildProgressListener))
processHandler.startNotify()
}
Expand Down

0 comments on commit d6ec3ce

Please sign in to comment.