fancy error printing done
This commit is contained in:
39
README.md
39
README.md
@@ -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:
|
||||

|
||||
### Either
|
||||
No example yet
|
||||
|
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
|
17
src/main/kotlin/blitz/collections/Bounds.kt
Normal file
17
src/main/kotlin/blitz/collections/Bounds.kt
Normal 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
|
||||
}
|
22
src/main/kotlin/blitz/collections/MergeNeighbors.kt
Normal file
22
src/main/kotlin/blitz/collections/MergeNeighbors.kt
Normal 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)
|
21
src/main/kotlin/blitz/str/ColoredChar.kt
Normal file
21
src/main/kotlin/blitz/str/ColoredChar.kt
Normal 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 = "")
|
109
src/main/kotlin/blitz/str/MutMultiColoredMultiLineString.kt
Normal file
109
src/main/kotlin/blitz/str/MutMultiColoredMultiLineString.kt
Normal 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")
|
||||
}
|
92
src/main/kotlin/blitz/str/MutMultiColoredString.kt
Normal file
92
src/main/kotlin/blitz/str/MutMultiColoredString.kt
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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 =
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
||||
|
Reference in New Issue
Block a user