Skip to content

Commit

Permalink
[Gradle] Add styling for ToolingDiagnostic messages
Browse files Browse the repository at this point in the history
^KT-73906
  • Loading branch information
AYastrebov authored and Space Team committed Feb 13, 2025
1 parent fcf7d45 commit 778115e
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 2 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/

package org.jetbrains.kotlin.gradle.plugin.diagnostics

import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.blue
import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.bold
import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.green
import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.italic
import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.lightBlue
import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.orange
import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.red
import org.jetbrains.kotlin.gradle.plugin.diagnostics.TerminalColorSupport.TerminalStyle.yellow
import org.jetbrains.kotlin.gradle.plugin.diagnostics.ToolingDiagnostic.Severity.*

/**
* Represents diagnostic icons used to indicate the severity level of diagnostics.
*
* @property icon The visual representation of the diagnostic icon, such as a warning or error symbol.
*/
enum class DiagnosticIcon(val icon: String) {
WARNING("⚠️"),
ERROR(""),
}

/**
* Represents a diagnostic message in a styled form, intended for tools and plugins.
*
* Provides information about the diagnostic, including a name, a message describing the issue,
* an optional solution, and optional documentation references.
*
* @property name The name of the diagnostic, providing a brief identifier for the issue.
* @property message The descriptive message explaining the diagnostic or issue.
* @property solution An optional proposed solution or recommended steps to resolve the issue.
* @property documentation Optional documentation reference offering additional context or resources.
*/
interface StyledToolingDiagnostic {
val name: String
val message: String
val solution: String?
val documentation: String?
}

/**
* This class provides a styled implementation of the `StyledToolingDiagnostic` interface.
*
* It wraps a `ToolingDiagnostic` to present its fields in a styled format
* through methods and properties like `name`, `message`, `solution`, and `documentation`.
*
* @constructor Creates an instance of `StyledToolingDiagnosticImp` using a `ToolingDiagnostic`.
* @param diagnostic The `ToolingDiagnostic` instance containing raw diagnostic data.
*
* The following details are styled:
* - The `name` is constructed with a severity-based icon and a colored identifier name.
* - The `message` is formatted with bold text.
* - The `solution` is presented in a formatted list or as a single-line message, with green styling.
* - The `documentation` is styled in blue, if available.
*
* Severity-based styling:
* - `WARNING`: Yellow text styling for the identifier name.
* - `ERROR` or `FATAL`: Red text styling for the identifier name.
*
* Solution presentation:
* - If one solution is present, it is labeled "Solution" and italicized.
* - If multiple solutions exist, each is listed with a bullet point, italicized, and styled in green.
*/
private class StyledToolingDiagnosticImp(private val diagnostic: ToolingDiagnostic) : StyledToolingDiagnostic {
override val name: String get() = buildName()
override val message: String get() = buildMessage()
override val solution: String? get() = buildSolution()
override val documentation: String? get() = buildDocumentation()

private fun buildName(): String {
val icon = when (diagnostic.severity) {
WARNING -> DiagnosticIcon.WARNING
else -> DiagnosticIcon.ERROR
}
return buildString {
append(icon.icon)
append(" ")
append(diagnostic.identifier.displayName.bold().let {
when (diagnostic.severity) {
WARNING -> it.yellow()
ERROR, FATAL -> it.red()
}
})
}
}

private fun buildMessage(): String {
// Optional: Early return for messages without code blocks
if (!diagnostic.message.contains("```")) {
return diagnostic.message.bold()
}

var inCodeBlock = false
val lines = diagnostic.message.lines()
return buildString {
for (line in lines) {
when {
line.trim() == "```" -> {
inCodeBlock = !inCodeBlock
continue
}
inCodeBlock -> appendLine(line.orange())
else -> appendLine(line.bold())
}
}
}.trimEnd()
}

private fun buildSolution(): String? {
val solutions = diagnostic.solutions
if (solutions.isEmpty()) return null

return buildString {
val prefix = if (solutions.size == 1) "Solution" else "Solutions"
appendLine("$prefix:".bold().green())

if (solutions.size == 1) {
append(solutions.single().italic().green())
} else {
solutions.forEach { solution ->
appendLine("${solution.italic()}".green())
}
}
}.trimEnd()
}

private fun buildDocumentation(): String? =
diagnostic.documentation?.let {
val highLightedUrl = it.url.blue()
it.additionalUrlContext.replace(it.url, highLightedUrl).lightBlue()
}
}

internal fun ToolingDiagnostic.styled(): StyledToolingDiagnostic = StyledToolingDiagnosticImp(this)
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright 2010-2024 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/

package org.jetbrains.kotlin.gradle.plugin.diagnostics

import org.jetbrains.kotlin.konan.target.HostManager

object TerminalColorSupport {
/**
* Enum representing various types of terminal environments.
*
* This enum class is used to differentiate between terminal environments
* typically encountered across different operating systems.
*
* - `WINDOWS_CMD`: Represents the Windows Command Prompt environment.
* - `WINDOWS_POWERSHELL`: Represents the Windows PowerShell environment.
* - `UNIX_LIKE`: Represents Unix-like terminal environments including Linux, macOS, and other Unix-based systems.
* - `UNKNOWN`: Represents an unrecognized or unsupported terminal environment.
*/
enum class TerminalType {
WINDOWS_CMD,
WINDOWS_POWERSHELL,
UNIX_LIKE,
UNKNOWN
}

private fun detectTerminalType() = when {
HostManager.hostIsMingw -> {
when {
System.getenv("PROMPT") != null -> TerminalType.WINDOWS_CMD
System.getenv("PSModulePath") != null -> TerminalType.WINDOWS_POWERSHELL
else -> TerminalType.UNKNOWN
}
}
HostManager.hostIsLinux || HostManager.hostIsMac -> TerminalType.UNIX_LIKE
else -> TerminalType.UNKNOWN
}

private fun supportsColor() = when (detectTerminalType()) {
TerminalType.WINDOWS_CMD -> false // No native ANSI support
TerminalType.WINDOWS_POWERSHELL -> true // Supports ANSI from PS 6.0+
TerminalType.UNIX_LIKE -> true
TerminalType.UNKNOWN -> false
}

/**
* Provides ANSI escape codes for applying various styles and colors to terminal text.
* Includes constants for commonly used styles and colors as well as extension functions
* to easily style strings.
*
* The object is designed to simplify the process of formatting text for terminal output.
* Reset codes are automatically appended after applying styles to ensure proper formatting.
*/
object TerminalStyle {
// ANSI color and style constants
private const val RESET = "\u001B[0m"
private const val YELLOW = "\u001B[33m"
private const val GREEN = "\u001B[32m"
private const val BOLD = "\u001B[1m"
private const val ITALIC = "\u001B[3m"
private const val RED = "\u001B[31m"
private const val BLUE = "\u001B[34m"
private const val LIGHT_BLUE = "\u001B[36m"
private const val ORANGE = "\u001B[38;5;214m"

// Convenience extension functions for styling
fun String.bold() = if (supportsColor()) "$BOLD$this$RESET" else ""
fun String.italic() = if (supportsColor()) "$ITALIC$this$RESET" else ""
fun String.yellow() = if (supportsColor()) "$YELLOW$this$RESET" else ""
fun String.green() = if (supportsColor()) "$GREEN$this$RESET" else ""
fun String.red() = if (supportsColor()) "$RED$this$RESET" else ""
fun String.blue() = if (supportsColor()) "$BLUE$this$RESET" else ""
fun String.lightBlue() = if (supportsColor()) "$LIGHT_BLUE$this$RESET" else ""
fun String.orange() = if (supportsColor()) "$ORANGE$this$RESET" else ""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ private fun ToolingDiagnostic.render(
renderingOptions: ToolingDiagnosticRenderingOptions,
showStacktrace: Boolean = renderingOptions.showStacktrace,
): String = buildString {
val styledDiagnostic = styled()
with(renderingOptions) {
if (useParsableFormat) {
appendLine(this@render)
Expand All @@ -80,8 +81,8 @@ private fun ToolingDiagnostic.render(
solutions.filter { it.isNotBlank() }.forEach {
appendLine(it)
}
documentation?.let {
appendLine(it.additionalUrlContext)
styledDiagnostic.documentation?.let {
appendLine(it)
}
}

Expand Down

0 comments on commit 778115e

Please sign in to comment.