fancy error printing done

This commit is contained in:
alexander.nutz
2024-03-29 12:21:09 +01:00
parent 355289716b
commit 19414c084c
12 changed files with 439 additions and 31 deletions

View File

@@ -98,5 +98,44 @@ val b = arrayOf(1, 2, 3, 4)
println(a.contents == b.contents) // true println(a.contents == b.contents) // true
println(b.contents) // [1, 2, 3, 4] println(b.contents) // [1, 2, 3, 4]
``` ```
### Code error messages
````kotlin
val source = Errors.Source("main.kt", MutMultiLineString.from("""
fn main() {
return 1 + 0
}
""".trimIndent(), ' '))
val errors = listOf(
Errors.Error(
"cannot return integer from function with return type void",
Errors.Error.Level.ERROR,
Errors.Location(source, 1, 11, 5)
),
Errors.Error(
"return is deprecated. use yeet instead",
Errors.Error.Level.WARN,
Errors.Location(source, 1, 4, 6)
),
Errors.Error(
"useless addition",
Errors.Error.Level.INFO,
Errors.Location(source, 1, 13, 3),
isHint = true
),
Errors.Error(
"Visit https://www.example.com/doc/yeet for more information",
Errors.Error.Level.INFO,
Errors.Location(source, 1, 0, 0),
isLongDesc = true
)
)
val config = Errors.PrintConfig()
Errors.print(config, errors)
````
Output:
![img.png](img.png)
### Either ### Either
No example yet No example yet

BIN
img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -1,5 +1,8 @@
package blitz.codeerrors package blitz.codeerrors
import blitz.collections.inBounds
import blitz.str.ColoredChar
import blitz.str.MutMultiColoredMultiLineString
import blitz.str.MutMultiLineString import blitz.str.MutMultiLineString
import blitz.str.MutString import blitz.str.MutString
import blitz.term.AnsiiMode import blitz.term.AnsiiMode
@@ -23,6 +26,7 @@ object Errors {
val level: Level, val level: Level,
val loc: Location, val loc: Location,
val isHint: Boolean = false, val isHint: Boolean = false,
val isLongDesc: Boolean = false,
) { ) {
enum class Level { enum class Level {
INFO, INFO,
@@ -41,19 +45,19 @@ object Errors {
data class PrintConfig( data class PrintConfig(
val styles: Map<Error.Level, AnsiiMode> = mapOf( val styles: Map<Error.Level, AnsiiMode> = mapOf(
Error.Level.INFO to Terminal.COLORS.WHITE.fg, Error.Level.INFO to Terminal.COLORS.MAGENTA.fg,
Error.Level.WARN to Terminal.COLORS.YELLOW.fg, Error.Level.WARN to Terminal.COLORS.YELLOW.fg,
Error.Level.ERROR to Terminal.COLORS.RED.fg Error.Level.ERROR to Terminal.COLORS.RED.fg
), ),
val levelStr: (Error.Level) -> String = { val levelStr: (Error.Level) -> String = {
when (it) { when (it) {
Error.Level.INFO -> "note" Error.Level.INFO -> "Note"
Error.Level.WARN -> "warning" Error.Level.WARN -> "Warning"
Error.Level.ERROR -> "error" Error.Level.ERROR -> "Error"
} }
}, },
val underlineString: (len: Int) -> String = { val underlineString: (len: Int) -> String = {
if (it == 0) "" else "^" + "~".repeat(it - 1) if (it == 0) "" else "^" + "^".repeat(it - 1)
}, },
) )
@@ -63,40 +67,90 @@ object Errors {
val worst = errors val worst = errors
.map { it.level } .map { it.level }
.reduce { acc, level -> acc + level } .reduce { acc, level -> acc + level }
Terminal.errln("File: \"${source.file}\"", config.styles[worst]!!) Terminal.errln("File: \"${source.file}\"", config.styles[worst]!! + Terminal.STYLES.BOLD)
Terminal.errln("================================================================================", config.styles[worst]!!) Terminal.errln("================================================================================", config.styles[worst]!! + Terminal.STYLES.BOLD)
val perLines = errors val perLinesMap = errors
.groupBy { it.loc.line } .groupBy { it.loc.line }
val perLines = perLinesMap
.entries .entries
.sortedBy { it.key } .sortedBy { it.key }
perLines.forEach { (line, errors) -> perLines.forEachIndexed { index, (line, errors) ->
errors.asSequence().filterNot { it.isHint }.forEach { err -> if (index > 0)
Terminal.errln("")
errors.asSequence().filterNot { it.isHint || it.isLongDesc }.forEach { err ->
Terminal.err(config.levelStr(err.level), config.styles[err.level]!!, Terminal.STYLES.BOLD) Terminal.err(config.levelStr(err.level), config.styles[err.level]!!, Terminal.STYLES.BOLD)
Terminal.errln(": ${err.message}", Terminal.STYLES.BOLD) Terminal.errln(": ${err.message}", Terminal.COLORS.WHITE.brighter.fg, Terminal.STYLES.BOLD)
} }
val msg = MutMultiLineString(' ') val printPrev = line > 0 && !perLinesMap.containsKey(line - 1)
val lineStr = line.toString() val printNext = source.content.lines.inBounds(line + 1) && !perLinesMap.containsKey(line + 1)
msg[1, 2] = lineStr
var nextCol = 3 + lineStr.length val worstLine = config.styles[
msg[0, nextCol] = '|' // TODO: print above and below source but dimmed? errors
msg[1, nextCol] = '|' .map { it.level }
msg[2, nextCol] = '|' .reduce { acc, level -> acc + level }
]!!
val msg = MutMultiColoredMultiLineString(fill = ColoredChar(' '))
val lineStr = (line + 1).toString()
msg.set(1, 2, lineStr, worstLine)
var nextCol = if (printNext) {
val nextLineStr = (line + 2).toString()
msg.set(2, 2, nextLineStr, Terminal.COLORS.WHITE.fg)
3 + nextLineStr.length
} else {
3 + lineStr.length
}
if (printPrev)
msg.set(0, 2, line.toString(), Terminal.COLORS.WHITE.fg)
fun pipe(row: Int, col: Int) =
msg.set(row, col, '|', Terminal.COLORS.WHITE.fg + Terminal.STYLES.BOLD)
pipe(0, nextCol)
msg.set(1, nextCol, '|', worstLine + Terminal.STYLES.BOLD)
pipe(2, nextCol)
nextCol += 2 nextCol += 2
if (line > 0) if (printPrev)
msg[0, nextCol] = source.content[line - 1] msg.set(0, nextCol, source.content[line - 1].toString(), Terminal.COLORS.WHITE.fg)
msg[1, nextCol] = source.content[line] msg.set(1, nextCol, source.content[line].toString(), Terminal.COLORS.WHITE.brighter.fg)
if (printNext)
msg.set(2, nextCol, source.content[line + 1].toString(), Terminal.COLORS.WHITE.fg)
// TODO: underline val byCol = errors.asSequence().sortedBy { it.loc.col }
byCol.filterNot { it.isHint || it.isLongDesc }.forEach {
msg.set(2, it.loc.col + nextCol, config.underlineString(it.loc.size), config.styles[it.level]!! + Terminal.STYLES.BOLD)
}
// TODO: hints var row = 3
byCol.filter { it.isHint }.forEach {
msg.set(row, it.loc.col + nextCol, config.underlineString(it.loc.size), config.styles[it.level]!! + Terminal.STYLES.BOLD)
val end = it.loc.col + nextCol + it.loc.size
msg.set(row, end + 1, it.message, config.styles[it.level]!!)
pipe(row, nextCol - 2)
row ++
}
row ++
byCol.filter { it.isLongDesc }.forEach {
val msgLines = MutMultiLineString.from(it.message, fill = ' ')
msg.set(row, nextCol, msgLines, Terminal.COLORS.WHITE.brighter.fg)
row ++
}
Terminal.errln(msg.toString()) Terminal.errln(msg.toString())
} }
Terminal.errln("================================================================================", config.styles[worst]!!) Terminal.errln("================================================================================", config.styles[worst]!! + Terminal.STYLES.BOLD)
} }
} }
} }
@@ -104,15 +158,32 @@ object Errors {
fun main() { fun main() {
val source = Errors.Source("main.kt", MutMultiLineString.from(""" val source = Errors.Source("main.kt", MutMultiLineString.from("""
fn main() { fn main() {
return 1 return 1 + 0
} }
""".trimIndent(), ' ')) """.trimIndent(), ' '))
val errors = listOf( val errors = listOf(
Errors.Error( Errors.Error(
"Cannot return integer from function with return type void", "cannot return integer from function with return type void",
Errors.Error.Level.ERROR, Errors.Error.Level.ERROR,
Errors.Location(source, 1, 11, 1) Errors.Location(source, 1, 11, 5)
),
Errors.Error(
"return is deprecated. use yeet instead",
Errors.Error.Level.WARN,
Errors.Location(source, 1, 4, 6)
),
Errors.Error(
"useless addition",
Errors.Error.Level.INFO,
Errors.Location(source, 1, 13, 3),
isHint = true
),
Errors.Error(
"Visit https://www.example.com/doc/yeet for more information",
Errors.Error.Level.INFO,
Errors.Location(source, 1, 0, 0),
isLongDesc = true
) )
) )

View File

@@ -0,0 +1,17 @@
package blitz.collections
fun <T> List<T>.inBounds(index: Int): Boolean {
if (index >= size)
return false
if (index < 0)
return false
return true
}
fun <T> Vec<T>.inBounds(index: Int): Boolean {
if (index >= size)
return false
if (index < 0)
return false
return true
}

View File

@@ -0,0 +1,22 @@
package blitz.collections
fun <T, G> Iterable<T>.mergeNeighbors(
to: MutableList<Pair<G, MutableList<T>>> = mutableListOf(),
by: (T) -> G
): MutableList<Pair<G, MutableList<T>>> {
val out = mutableListOf<Pair<G, MutableList<T>>>()
forEach {
val b = by(it)
if (b == out.lastOrNull()?.first)
out.last().second.add(it)
else
out.add(b to mutableListOf(it))
}
return out
}
fun <T, G> Sequence<T>.mergeNeighbors(
to: MutableList<Pair<G, MutableList<T>>> = mutableListOf(),
by: (T) -> G
): MutableList<Pair<G, MutableList<T>>> =
asIterable().mergeNeighbors(to, by)

View File

@@ -0,0 +1,21 @@
package blitz.str
import blitz.term.AnsiiMode
import blitz.term.Terminal
class ColoredChar(
val char: Char,
val style: AnsiiMode = AnsiiMode(mutableListOf())
) {
override fun equals(other: Any?): Boolean =
char == other
override fun hashCode(): Int =
char.hashCode()
override fun toString(): String =
Terminal.encodeString("$char", style)
}
fun Iterable<ColoredChar>.convToString(): String =
joinToString(separator = "")

View File

@@ -0,0 +1,109 @@
package blitz.str
import blitz.term.AnsiiMode
class MutMultiColoredMultiLineString(
var fill: ColoredChar
) {
val lines = mutableListOf<MutMultiColoredString>()
// TODO: wrap at \n
override fun equals(other: Any?): Boolean =
lines == other
override fun hashCode(): Int =
lines.hashCode()
/** if out of bounds, extends with @see fill */
operator fun get(row: Int, col: Int): ColoredChar {
if (row >= lines.size) {
repeat(row - lines.size + 1) {
lines.add(MutMultiColoredString(fill = fill))
}
}
return lines[row][col]
}
/** if out of bounds, extends with @see fill */
operator fun get(row: Int): MutMultiColoredString {
if (row >= lines.size) {
repeat(row - lines.size + 1) {
lines.add(MutMultiColoredString(fill = fill))
}
}
return lines[row]
}
/** if out of bounds, extends with @see fill */
operator fun set(row: Int, col: Int, value: ColoredChar) {
if (row >= lines.size) {
repeat(row - lines.size + 1) {
lines.add(MutMultiColoredString(fill = fill))
}
} else {
lines[row].fill = fill
}
lines[row][col] = value
}
/** if out of bounds, extends with @see fill */
operator fun set(row: Int, col: Int, value: Char) =
set(row, col, ColoredChar(value))
/** if out of bounds, extends with @see fill */
operator fun set(row: Int, colStart: Int, value: CharSequence) {
if (row >= lines.size) {
repeat(row - lines.size + 1) {
lines.add(MutMultiColoredString(fill = fill))
}
} else {
lines[row].fill = fill
}
lines[row][colStart] = value
}
/** if out of bounds, extends with @see fill */
operator fun set(row: Int, colStart: Int, value: MutMultiColoredString) {
if (row >= lines.size) {
repeat(row - lines.size + 1) {
lines.add(MutMultiColoredString(fill = fill))
}
} else {
lines[row].fill = fill
}
lines[row][colStart] = value
}
/** if out of bounds, extends with @see fill */
fun set(row: Int, colStart: Int, va: Char, style: AnsiiMode) =
set(row, colStart, ColoredChar(va, style))
/** if out of bounds, extends with @see fill */
fun set(row: Int, colStart: Int, va: String, style: AnsiiMode) =
set(row, colStart, MutMultiColoredString.from(va, style))
/** if out of bounds, extends with @see fill */
operator fun set(rowStart: Int, colStart: Int, value: MutMultiLineString) {
value.lines.forEachIndexed { index, line ->
this[index + rowStart, colStart] = line
}
}
/** if out of bounds, extends with @see fill */
operator fun set(rowStart: Int, colStart: Int, value: MutMultiLineString, style: AnsiiMode) {
value.lines.forEachIndexed { index, line ->
this.set(index + rowStart, colStart, line.toString(), style)
}
}
/** if out of bounds, extends with @see fill */
operator fun set(rowStart: Int, colStart: Int, value: MutMultiColoredMultiLineString) {
value.lines.forEachIndexed { index, line ->
this[index + rowStart, colStart] = line
}
}
override fun toString(): String =
lines.joinToString(separator = "\n")
}

View File

@@ -0,0 +1,92 @@
package blitz.str
import blitz.collections.mergeNeighbors
import blitz.term.AnsiiMode
import blitz.term.Terminal
class MutMultiColoredString(var fill: ColoredChar) {
val chars = mutableListOf<ColoredChar>()
val length
get() = chars.size
override fun equals(other: Any?): Boolean =
chars == other
override fun hashCode(): Int =
chars.hashCode()
override fun toString(): String {
val byColors = chars.mergeNeighbors { it.style }
val res = MutString(fill = ' ')
byColors.forEach {
val style = it.first
val str = it.second.convToString()
res.append(Terminal.encodeString(str, style))
}
return res.toString()
}
fun add(str: String, style: AnsiiMode = AnsiiMode(mutableListOf())) {
str.mapTo(chars) { ColoredChar(it, style) }
}
fun add(str: Iterable<ColoredChar>) {
chars.addAll(str)
}
fun add(ch: ColoredChar) {
chars.add(ch)
}
operator fun get(index: Int): ColoredChar {
if (index >= length) {
repeat(index - length + 1) {
chars.add(fill)
}
}
return chars[index]
}
operator fun set(index: Int, value: ColoredChar) {
if (index >= length) {
repeat(index - length + 1) {
chars.add(fill)
}
}
chars[index] = value
}
fun set(start: Int, str: CharSequence, style: AnsiiMode) {
if (start + str.length >= length) {
repeat(start + str.length - length + 1) {
chars.add(fill)
}
}
str.forEachIndexed { index, c ->
chars[start + index] = ColoredChar(c, style)
}
}
operator fun set(start: Int, str: CharSequence) {
set(start, str, AnsiiMode(mutableListOf()))
}
operator fun set(start: Int, str: MutMultiColoredString) {
if (start + str.length >= length) {
repeat(start + str.length - length + 1) {
chars.add(fill)
}
}
str.chars.forEachIndexed { index, c ->
chars[start + index] = c
}
}
companion object {
fun from(str: String, style: AnsiiMode = AnsiiMode(mutableListOf())) =
MutMultiColoredString(fill = ColoredChar(' ')).also {
it.add(str, style)
}
}
}

View File

@@ -7,6 +7,12 @@ class MutMultiLineString(
// TODO: wrap at \n // TODO: wrap at \n
override fun equals(other: Any?): Boolean =
lines == other
override fun hashCode(): Int =
lines.hashCode()
/** if out of bounds, extends with @see fill */ /** if out of bounds, extends with @see fill */
operator fun get(row: Int, col: Int): Char { operator fun get(row: Int, col: Int): Char {
if (row >= lines.size) { if (row >= lines.size) {

View File

@@ -1,11 +1,19 @@
package blitz.str package blitz.str
import java.util.stream.IntStream
class MutString( class MutString(
init: String = "", init: String = "",
var fill: Char var fill: Char
): CharSequence, Appendable { ): CharSequence, Appendable {
private val builder = StringBuilder(init) private val builder = StringBuilder(init)
override fun chars(): IntStream =
builder.chars()
override fun codePoints(): IntStream =
builder.codePoints()
override val length: Int override val length: Int
get() = builder.length get() = builder.length
@@ -30,12 +38,12 @@ class MutString(
/** if out of bounds, extends with @see fill */ /** if out of bounds, extends with @see fill */
operator fun set(start: Int, str: CharSequence) { operator fun set(start: Int, str: CharSequence) {
if (start >= length) { if (start + str.length >= length) {
repeat(start - length + 1) { repeat(start + str.length - length + 1) {
builder.append(fill) builder.append(fill)
} }
} }
builder.insert(start, str) builder.replace(start, start + str.length, str.toString())
} }
override fun toString(): String = override fun toString(): String =

View File

@@ -5,11 +5,17 @@ class AnsiiMode(internal val values: MutableList<Int>) {
operator fun plus(other: AnsiiMode): AnsiiMode = operator fun plus(other: AnsiiMode): AnsiiMode =
AnsiiMode((values + other.values).toMutableList()) AnsiiMode((values + other.values).toMutableList())
override fun equals(other: Any?): Boolean =
values == other
override fun hashCode(): Int =
values.hashCode()
} }
private val escape = (27).toChar() private val escape = (27).toChar()
fun ansiiStr(str: String, vararg modes: AnsiiMode) = internal fun ansiiStr(str: String, vararg modes: AnsiiMode) =
if (modes.isEmpty()) if (modes.isEmpty())
str str
else else

View File

@@ -38,6 +38,23 @@ object Terminal {
val darker by lazy { Color(AnsiiMode(darkerChannel(ch(fg))), AnsiiMode(darkerChannel(ch(bg)))) } val darker by lazy { Color(AnsiiMode(darkerChannel(ch(fg))), AnsiiMode(darkerChannel(ch(bg)))) }
} }
/** Escape-sequence safe string length */
fun len(str: String): Int {
var len = 0
var ansii = false
str.forEach {
if (ansii) {
if (it == 'm')
ansii = false
} else if (it.code == 27) {
ansii = true
} else {
len ++
}
}
return len
}
fun encodeString(str: String, vararg modes: AnsiiMode) = fun encodeString(str: String, vararg modes: AnsiiMode) =
ansiiStr(str, *modes) ansiiStr(str, *modes)