Skip to content

Commit

Permalink
RUN: Support for ANSI colors in Build tool window
Browse files Browse the repository at this point in the history
  • Loading branch information
mchernyavsky committed Dec 17, 2020
1 parent f5768a5 commit 735ce85
Show file tree
Hide file tree
Showing 14 changed files with 143 additions and 136 deletions.
20 changes: 11 additions & 9 deletions src/main/kotlin/org/rust/cargo/runconfig/CargoRunStateBase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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 @@ -58,16 +58,18 @@ abstract class CargoRunStateBase(
return commandLine
}

override fun startProcess(): ProcessHandler = startProcess(emulateTerminal = false)
override fun startProcess(): ProcessHandler = startProcess(processColors = true)

fun startProcess(emulateTerminal: Boolean): ProcessHandler {
var commandLine = cargo().toColoredCommandLine(environment.project, prepareCommandLine())
if (emulateTerminal) {
commandLine = PtyCommandLine(commandLine)
.withInitialColumns(PtyCommandLine.MAX_COLUMNS)
.withConsoleMode(false)
/**
* @param processColors if true, process ANSI escape sequences, otherwise keep escape codes in the output
*/
fun startProcess(processColors: Boolean): ProcessHandler {
val commandLine = cargo().toColoredCommandLine(environment.project, prepareCommandLine())
val handler = if (processColors) {
RsKillableColoredProcessHandler(commandLine)
} else {
KillableProcessHandler(commandLine)
}
val handler = RsKillableColoredProcessHandler(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 @@ -53,7 +53,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(processColors = true)
}
val exitCode = AsyncPromise<Int?>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class CargoTestRunState(
post.add("unstable-options")
}

addFormatJsonOption(post, "--format")
addFormatJsonOption(post, "--format", "json")

if (checkShowOutputSupport(rustcVer)) {
post.add("--show-output")
Expand Down
17 changes: 14 additions & 3 deletions src/main/kotlin/org/rust/cargo/runconfig/RsAnsiEscapeDecoder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,22 @@
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
import org.rust.stdext.nextOrNull
import java.awt.Color
import kotlin.math.roundToInt

fun AnsiEscapeDecoder.removeEscapeSequences(text: String): String {
val chunks = mutableListOf<String>()
escapeText(text, ProcessOutputTypes.STDOUT) { chunk, _ ->
chunks.add(chunk)
}
return chunks.joinToString("")
}

/**
* Currently IntelliJ Platform supports only 16 ANSI colors (standard colors and high intensity colors). The base
* [AnsiEscapeDecoder] class simply ignores 8-bit and 24-bit ANSI color escapes. This class converts (quantizes) such
Expand All @@ -27,7 +36,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 @@ -41,8 +52,8 @@ class RsAnsiEscapeDecoder : AnsiEscapeDecoder() {
*
* @param text a string with ANSI escape sequences
*/
private fun quantizeAnsiColors(text: String): String = text
.replace(ANSI_CONTROL_SEQUENCE_REGEX) {
fun quantizeAnsiColors(text: String): String = text
.replace(ANSI_SGR_RE) {
val rawAttributes = it.destructured.component1().split(";").iterator()
val result = mutableListOf<Int>()
while (rawAttributes.hasNext()) {
Expand Down
10 changes: 5 additions & 5 deletions src/main/kotlin/org/rust/cargo/runconfig/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -94,19 +94,19 @@ fun createFilters(cargoProject: CargoProject?): Collection<Filter> = buildList {
}
}

fun addFormatJsonOption(additionalArguments: MutableList<String>, formatOption: String) {
val formatJsonOption = "$formatOption=json"
fun addFormatJsonOption(additionalArguments: MutableList<String>, formatOption: String, format: String) {
val formatJsonOption = "$formatOption=$format"
val idx = additionalArguments.indexOf(formatOption)
val indexArgWithValue = additionalArguments.indexOfFirst { it.startsWith(formatOption) }
if (idx != -1) {
if (idx < additionalArguments.size - 1) {
if (!additionalArguments[idx + 1].startsWith("-")) {
additionalArguments[idx + 1] = "json"
additionalArguments[idx + 1] = format
} else {
additionalArguments.add(idx + 1, "json")
additionalArguments.add(idx + 1, format)
}
} else {
additionalArguments.add("json")
additionalArguments.add(format)
}
} else if (indexArgWithValue != -1) {
additionalArguments[indexArgWithValue] = formatJsonOption
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,32 +21,24 @@ import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.util.Key
import com.intellij.openapi.util.SystemInfo
import com.intellij.openapi.util.text.StringUtil
import com.intellij.openapi.util.text.StringUtil.convertLineSeparators
import com.intellij.openapi.vfs.VfsUtil
import org.rust.cargo.CargoConstants
import org.rust.cargo.runconfig.RsAnsiEscapeDecoder
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()

private val buildOutputParser: CargoBuildEventsConverter = CargoBuildEventsConverter(context)

) : ProcessAdapter() {
private val instantReader = BuildOutputInstantReaderImpl(
context.buildId,
context.buildId,
buildProgressListener,
listOf(buildOutputParser)
listOf(RsBuildEventsConverter(context))
)

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

init {
val processHandler = checkNotNull(context.processHandler) { "Process handler can't be null" }
context.environment.notifyProcessStarted(processHandler)
Expand All @@ -67,7 +59,6 @@ class CargoBuildAdapter(
instantReader.closeAndGetFuture().whenComplete { _, error ->
val isSuccess = event.exitCode == 0 && context.errors == 0
val isCanceled = context.indicator?.isCanceled ?: false
context.environment.binaries = buildOutputParser.binaries.takeIf { isSuccess && !isCanceled }

val (status, result) = when {
isCanceled -> "canceled" to SkippedResultImpl()
Expand Down Expand Up @@ -97,39 +88,13 @@ 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<*>) {
// Progress messages end with '\r' instead of '\n'. We want to replace '\r' with '\n'
// so that `instantReader` sends progress messages to parsers separately from other messages.
val text = convertLineSeparators(event.text)
instantReader.append(text)
}

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 fun createStopAction(processHandler: ProcessHandler): StopProcessAction =
StopProcessAction("Stop", "Stop", processHandler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import com.intellij.openapi.util.UserDataHolderEx
import com.intellij.openapiext.isUnitTestMode
import com.intellij.util.concurrency.FutureResult
import org.rust.cargo.project.model.CargoProject
import org.rust.cargo.runconfig.RsExecutableRunner.Companion.binaries
import org.rust.cargo.runconfig.buildtool.CargoBuildManager.showBuildNotification
import org.rust.cargo.runconfig.command.workingDirectory
import java.nio.file.Path
Expand All @@ -39,16 +40,25 @@ class CargoBuildContext(
private val buildSemaphore: Semaphore = project.getUserData(BUILD_SEMAPHORE_KEY)
?: (project as UserDataHolderEx).putUserDataIfAbsent(BUILD_SEMAPHORE_KEY, Semaphore(1))

@Volatile
var indicator: ProgressIndicator? = null
@Volatile
var processHandler: ProcessHandler? = null

val started: Long = System.currentTimeMillis()
@Volatile
var finished: Long = started
private val duration: Long get() = finished - started

@Volatile
var errors: Int = 0

@Volatile
var warnings: Int = 0

@Volatile
var binaries: List<Path> = emptyList()

fun waitAndStart(): Boolean {
indicator?.pushState()
try {
Expand All @@ -74,6 +84,8 @@ class CargoBuildContext(
fun finished(isSuccess: Boolean) {
val isCanceled = indicator?.isCanceled ?: false

environment.binaries = binaries.takeIf { isSuccess && !isCanceled }

finished = System.currentTimeMillis()
buildSemaphore.release()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import com.intellij.openapiext.isHeadlessEnvironment
import com.intellij.openapiext.isUnitTestMode
import com.intellij.ui.SystemNotifications
import com.intellij.ui.content.MessageView
import com.intellij.util.EnvironmentUtil
import com.intellij.util.concurrency.FutureResult
import com.intellij.util.execution.ParametersListUtil
import com.intellij.util.text.SemVer
Expand Down Expand Up @@ -62,22 +61,21 @@ object CargoBuildManager {
val CANCELED_BUILD_RESULT: Future<CargoBuildResult> =
FutureResult(CargoBuildResult(succeeded = false, canceled = true, started = 0))

private val MIN_RUSTC_VERSION: SemVer = SemVer("1.32.0", 1, 32, 0)
private val MIN_RUSTC_VERSION: SemVer = SemVer.parseFromText("1.48.0")!!

val Project.isBuildToolWindowEnabled: Boolean
get() {
if (!isFeatureEnabled(RsExperiments.BUILD_TOOL_WINDOW)) return false
val rustcVersion = cargoProjects
.allProjects
val minVersion = cargoProjects.allProjects
.mapNotNull { it.rustcInfo?.version?.semver }
.min()
?: return false
return rustcVersion >= MIN_RUSTC_VERSION
.min() ?: return false
return minVersion >= MIN_RUSTC_VERSION
}

fun build(buildConfiguration: CargoBuildConfiguration): Future<CargoBuildResult> {
val configuration = buildConfiguration.configuration
val environment = buildConfiguration.environment
val project = environment.project

environment.cargoPatches += cargoBuildPatch
val state = CargoRunState(
Expand All @@ -89,7 +87,7 @@ object CargoBuildManager {
val cargoProject = state.cargoProject ?: return CANCELED_BUILD_RESULT

// Make sure build tool window is initialized:
ServiceManager.getService(cargoProject.project, BuildContentManager::class.java)
ServiceManager.getService(project, BuildContentManager::class.java)

if (isUnitTestMode) {
lastBuildCommandLine = state.commandLine
Expand All @@ -115,7 +113,7 @@ object CargoBuildManager {
buildToolWindow.show(null)
}

processHandler = state.startProcess(emulateTerminal = true)
processHandler = state.startProcess(processColors = false)
processHandler?.addProcessListener(CargoBuildAdapter(this, buildProgressListener))
processHandler?.startNotify()
}
Expand Down Expand Up @@ -278,15 +276,26 @@ object CargoBuildManager {
}

private val cargoBuildPatch: CargoPatch = { commandLine ->
val additionalArguments = commandLine.additionalArguments.toMutableList()
additionalArguments.remove("-q")
additionalArguments.remove("--quiet")
addFormatJsonOption(additionalArguments, "--message-format")
val additionalArguments = mutableListOf<String>().apply {
addAll(commandLine.additionalArguments)
remove("-q")
remove("--quiet")
// If `json-diagnostic-rendered-ansi` is used, `rendered` field of JSON messages contains
// embedded ANSI color codes for respecting rustc's default color scheme.
addFormatJsonOption(this, "--message-format", "json-diagnostic-rendered-ansi")
}

val oldVariables = commandLine.environmentVariables
val environmentVariables = EnvironmentVariablesData.create(
EnvironmentUtil.getEnvironmentMap() +
commandLine.environmentVariables.envs - "CI" + ("TERM" to "ansi"),
false
// https://doc.rust-lang.org/cargo/reference/environment-variables.html#configuration-environment-variables
// These environment variables are needed to force progress bar to non-TTY output
oldVariables.envs + mapOf(
"CARGO_TERM_PROGRESS_WHEN" to "always",
"CARGO_TERM_PROGRESS_WIDTH" to "1000"
),
oldVariables.isPassParentEnvs
)

commandLine.copy(additionalArguments = additionalArguments, environmentVariables = environmentVariables)
}

Expand Down

0 comments on commit 735ce85

Please sign in to comment.