diff --git a/README.md b/README.md index 4a68902..603d96b 100644 --- a/README.md +++ b/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: +![img.png](img.png) ### Either No example yet diff --git a/img.png b/img.png new file mode 100644 index 0000000..96c3905 Binary files /dev/null and b/img.png differ diff --git a/src/main/kotlin/blitz/codeerrors/Errors.kt b/src/main/kotlin/blitz/codeerrors/Errors.kt index 083e520..8a9aae8 100644 --- a/src/main/kotlin/blitz/codeerrors/Errors.kt +++ b/src/main/kotlin/blitz/codeerrors/Errors.kt @@ -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 = 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 ) ) diff --git a/src/main/kotlin/blitz/collections/Bounds.kt b/src/main/kotlin/blitz/collections/Bounds.kt new file mode 100644 index 0000000..c0954cf --- /dev/null +++ b/src/main/kotlin/blitz/collections/Bounds.kt @@ -0,0 +1,17 @@ +package blitz.collections + +fun List.inBounds(index: Int): Boolean { + if (index >= size) + return false + if (index < 0) + return false + return true +} + +fun Vec.inBounds(index: Int): Boolean { + if (index >= size) + return false + if (index < 0) + return false + return true +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/collections/MergeNeighbors.kt b/src/main/kotlin/blitz/collections/MergeNeighbors.kt new file mode 100644 index 0000000..3eaedf5 --- /dev/null +++ b/src/main/kotlin/blitz/collections/MergeNeighbors.kt @@ -0,0 +1,22 @@ +package blitz.collections + +fun Iterable.mergeNeighbors( + to: MutableList>> = mutableListOf(), + by: (T) -> G +): MutableList>> { + val out = mutableListOf>>() + 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 Sequence.mergeNeighbors( + to: MutableList>> = mutableListOf(), + by: (T) -> G +): MutableList>> = + asIterable().mergeNeighbors(to, by) \ No newline at end of file diff --git a/src/main/kotlin/blitz/str/ColoredChar.kt b/src/main/kotlin/blitz/str/ColoredChar.kt new file mode 100644 index 0000000..bf7ce4c --- /dev/null +++ b/src/main/kotlin/blitz/str/ColoredChar.kt @@ -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.convToString(): String = + joinToString(separator = "") \ No newline at end of file diff --git a/src/main/kotlin/blitz/str/MutMultiColoredMultiLineString.kt b/src/main/kotlin/blitz/str/MutMultiColoredMultiLineString.kt new file mode 100644 index 0000000..2786900 --- /dev/null +++ b/src/main/kotlin/blitz/str/MutMultiColoredMultiLineString.kt @@ -0,0 +1,109 @@ +package blitz.str + +import blitz.term.AnsiiMode + +class MutMultiColoredMultiLineString( + var fill: ColoredChar +) { + val lines = mutableListOf() + + // 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") +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/str/MutMultiColoredString.kt b/src/main/kotlin/blitz/str/MutMultiColoredString.kt new file mode 100644 index 0000000..916b263 --- /dev/null +++ b/src/main/kotlin/blitz/str/MutMultiColoredString.kt @@ -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() + + 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) { + 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) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/blitz/str/MutMultiLineString.kt b/src/main/kotlin/blitz/str/MutMultiLineString.kt index e4287ca..f2b5f59 100644 --- a/src/main/kotlin/blitz/str/MutMultiLineString.kt +++ b/src/main/kotlin/blitz/str/MutMultiLineString.kt @@ -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) { diff --git a/src/main/kotlin/blitz/str/MutString.kt b/src/main/kotlin/blitz/str/MutString.kt index 5a0a61e..b5414a0 100644 --- a/src/main/kotlin/blitz/str/MutString.kt +++ b/src/main/kotlin/blitz/str/MutString.kt @@ -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 = diff --git a/src/main/kotlin/blitz/term/AnsiiStr.kt b/src/main/kotlin/blitz/term/AnsiiStr.kt index 1475c07..05e9cb2 100644 --- a/src/main/kotlin/blitz/term/AnsiiStr.kt +++ b/src/main/kotlin/blitz/term/AnsiiStr.kt @@ -5,11 +5,17 @@ class AnsiiMode(internal val values: MutableList) { 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 diff --git a/src/main/kotlin/blitz/term/Terminal.kt b/src/main/kotlin/blitz/term/Terminal.kt index d7801eb..c97dfc4 100644 --- a/src/main/kotlin/blitz/term/Terminal.kt +++ b/src/main/kotlin/blitz/term/Terminal.kt @@ -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)