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(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
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
import blitz.collections.inBounds
import blitz.str.ColoredChar
import blitz.str.MutMultiColoredMultiLineString
import blitz.str.MutMultiLineString
import blitz.str.MutString
import blitz.term.AnsiiMode
@@ -23,6 +26,7 @@ object Errors {
val level: Level,
val loc: Location,
val isHint: Boolean = false,
val isLongDesc: Boolean = false,
) {
enum class Level {
INFO,
@@ -41,19 +45,19 @@ object Errors {
data class PrintConfig(
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.ERROR to Terminal.COLORS.RED.fg
),
val levelStr: (Error.Level) -> String = {
when (it) {
Error.Level.INFO -> "note"
Error.Level.WARN -> "warning"
Error.Level.ERROR -> "error"
Error.Level.INFO -> "Note"
Error.Level.WARN -> "Warning"
Error.Level.ERROR -> "Error"
}
},
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
.map { it.level }
.reduce { acc, level -> acc + level }
Terminal.errln("File: \"${source.file}\"", config.styles[worst]!!)
Terminal.errln("================================================================================", config.styles[worst]!!)
Terminal.errln("File: \"${source.file}\"", config.styles[worst]!! + Terminal.STYLES.BOLD)
Terminal.errln("================================================================================", config.styles[worst]!! + Terminal.STYLES.BOLD)
val perLines = errors
val perLinesMap = errors
.groupBy { it.loc.line }
val perLines = perLinesMap
.entries
.sortedBy { it.key }
perLines.forEach { (line, errors) ->
errors.asSequence().filterNot { it.isHint }.forEach { err ->
perLines.forEachIndexed { index, (line, errors) ->
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.errln(": ${err.message}", Terminal.STYLES.BOLD)
Terminal.errln(": ${err.message}", Terminal.COLORS.WHITE.brighter.fg, Terminal.STYLES.BOLD)
}
val msg = MutMultiLineString(' ')
val lineStr = line.toString()
msg[1, 2] = lineStr
var nextCol = 3 + lineStr.length
msg[0, nextCol] = '|' // TODO: print above and below source but dimmed?
msg[1, nextCol] = '|'
msg[2, nextCol] = '|'
val printPrev = line > 0 && !perLinesMap.containsKey(line - 1)
val printNext = source.content.lines.inBounds(line + 1) && !perLinesMap.containsKey(line + 1)
val worstLine = config.styles[
errors
.map { it.level }
.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
if (line > 0)
msg[0, nextCol] = source.content[line - 1]
msg[1, nextCol] = source.content[line]
if (printPrev)
msg.set(0, nextCol, source.content[line - 1].toString(), Terminal.COLORS.WHITE.fg)
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("================================================================================", config.styles[worst]!!)
Terminal.errln("================================================================================", config.styles[worst]!! + Terminal.STYLES.BOLD)
}
}
}
@@ -104,15 +158,32 @@ object Errors {
fun main() {
val source = Errors.Source("main.kt", MutMultiLineString.from("""
fn main() {
return 1
return 1 + 0
}
""".trimIndent(), ' '))
val errors = listOf(
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.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
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): Char {
if (row >= lines.size) {

View File

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

View File

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

View File

@@ -38,6 +38,23 @@ object Terminal {
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) =
ansiiStr(str, *modes)