From b505519b57df943c7f5e191ec4764068b9daa798 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 29 Oct 2025 11:00:58 +0800 Subject: [PATCH 1/5] . --- .../dotty/tools/dotc/util/StackTraceOps.scala | 42 +++++++----- compiler/src/dotty/tools/repl/Rendering.scala | 65 +++++++++++-------- .../src/dotty/tools/repl/ReplDriver.scala | 4 +- 3 files changed, 67 insertions(+), 44 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala index bd5c031a65e0..03fca0d05b3a 100644 --- a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala +++ b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala @@ -13,7 +13,7 @@ package dotty.tools.dotc.util import scala.language.unsafeNulls - +import dotty.shaded.fansi import collection.mutable, mutable.ListBuffer import dotty.tools.dotc.util.chaining.* import java.lang.System.lineSeparator @@ -31,23 +31,26 @@ object StackTraceOps: * shared stack trace segments. * @param p the predicate to select the prefix */ - def formatStackTracePrefix(p: StackTraceElement => Boolean): String = + def formatStackTracePrefix(p: StackTraceElement => Boolean): fansi.Str = type TraceRelation = String - val Self = new TraceRelation("") - val CausedBy = new TraceRelation("Caused by: ") - val Suppressed = new TraceRelation("Suppressed: ") + val Self = "" + val CausedBy = "Caused by: " + val Suppressed = "Suppressed: " - def header(e: Throwable): String = + def header(e: Throwable): fansi.Str = def because = e.getCause match { case null => null ; case c => header(c) } def msg = e.getMessage match { case null => because ; case s => s } - def txt = msg match { case null => "" ; case s => s": $s" } - s"${e.getClass.getName}$txt" + def txt = msg match { + case null => "" + case s => s": $s" + } + fansi.Color.LightRed(e.getClass.getName) ++ txt val seen = mutable.Set.empty[Throwable] def unseen(e: Throwable): Boolean = (e != null && !seen(e)).tap(if _ then seen += e) - val lines = ListBuffer.empty[String] + val lines = ListBuffer.empty[fansi.Str] // format the stack trace, skipping the shared trace def print(e: Throwable, r: TraceRelation, share: Array[StackTraceElement], indents: Int): Unit = if unseen(e) then @@ -58,20 +61,29 @@ object StackTraceOps: trimmed.reverse val prefix = frames.takeWhile(p) val margin = " " * indents - lines += s"${margin}${r}${header(e)}" - prefix.foreach(frame => lines += s"$margin at $frame") + lines += fansi.Str(margin) ++ fansi.Color.Red(r) ++ header(e) + prefix.foreach(frame => + lines += + fansi.Str(s"$margin ") ++ fansi.Color.Red("at") ++ " " ++ + fansi.Str.join(frame.getClassName.split('.').map(fansi.Color.LightRed(_)), ".") ++ "." ++ + fansi.Color.LightRed(frame.getMethodName) ++ "(" ++ + fansi.Color.LightRed(frame.getFileName) ++ ":" ++ + fansi.Color.LightRed(frame.getLineNumber.toString) ++ ")" + ) val traceFramesLenDiff = trace.length - frames.length val framesPrefixLenDiff = frames.length - prefix.length if traceFramesLenDiff > 0 then - if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided and $traceFramesLenDiff more" - else lines += s"$margin ... $traceFramesLenDiff more" - else if framesPrefixLenDiff > 0 then lines += s"$margin ... $framesPrefixLenDiff elided" + if framesPrefixLenDiff > 0 + then lines += fansi.Color.Red(s"$margin ... $framesPrefixLenDiff elided and $traceFramesLenDiff more") + else lines += fansi.Color.Red(s"$margin ... $traceFramesLenDiff more") + else if framesPrefixLenDiff > 0 + then lines += fansi.Color.Red(s"$margin ... $framesPrefixLenDiff elided") print(e.getCause, CausedBy, trace, indents) e.getSuppressed.foreach(print(_, Suppressed, frames, indents + 1)) end print print(t, Self, share = Array.empty, indents = 0) - lines.mkString(lineSeparator) + fansi.Str.join(lines, lineSeparator) end formatStackTracePrefix diff --git a/compiler/src/dotty/tools/repl/Rendering.scala b/compiler/src/dotty/tools/repl/Rendering.scala index a86ba12bb0a9..e0e57d4e0535 100644 --- a/compiler/src/dotty/tools/repl/Rendering.scala +++ b/compiler/src/dotty/tools/repl/Rendering.scala @@ -6,6 +6,8 @@ import scala.language.unsafeNulls import dotc.*, core.* import Contexts.*, Denotations.*, Flags.*, NameOps.*, StdNames.*, Symbols.* import printing.ReplPrinter +import printing.SyntaxHighlighting +import dotty.shaded.{fansi, pprint} import reporting.Diagnostic import util.StackTraceOps.* @@ -26,17 +28,21 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): var myClassLoader: AbstractFileClassLoader = uninitialized - private def pprintRender(value: Any, width: Int, height: Int, initialOffset: Int)(using Context): String = { - def fallback() = - dotty.shaded.pprint.PPrinter.BlackWhite + private def pprintRender(value: Any, width: Int, height: Int, initialOffset: Int)(using Context): fansi.Str = { + def fallback() = { + + val fansiStr = pprint.PPrinter.Color .apply(value, width = width, height = height, initialOffset = initialOffset) - .plainText + dotty.shaded.pprint.log(fansiStr.toString.toCharArray) + fansiStr + } + try val cl = classLoader() - val pprintCls = Class.forName("dotty.shaded.pprint.PPrinter$BlackWhite$", false, cl) - val fansiStrCls = Class.forName("dotty.shaded.fansi.Str", false, cl) - val BlackWhite = pprintCls.getField("MODULE$").get(null) - val BlackWhite_apply = pprintCls.getMethod("apply", + val pprintCls = Class.forName("pprint.PPrinter$Color$", false, cl) + val fansiStrCls = Class.forName("fansi.Str", false, cl) + val Color = pprintCls.getField("MODULE$").get(null) + val Color_apply = pprintCls.getMethod("apply", classOf[Any], // value classOf[Int], // width classOf[Int], // height @@ -45,11 +51,11 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): classOf[Boolean], // escape Unicode classOf[Boolean], // show field names ) - val FansiStr_plainText = fansiStrCls.getMethod("plainText") - val fansiStr = BlackWhite_apply.invoke( - BlackWhite, value, width, height, 2, initialOffset, false, true + val fansiStr = Color_apply.invoke( + Color, value, width, height, 2, initialOffset, false, true ) - FansiStr_plainText.invoke(fansiStr).asInstanceOf[String] + dotty.shaded.pprint.log(fansiStr.toString.toCharArray) + fansi.Str(fansiStr.toString) catch case _: ClassNotFoundException => fallback() case _: NoSuchMethodException => fallback() @@ -85,8 +91,8 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): if ncp <= maxPrintCharacters then str else str.substring(0, str.offsetByCodePoints(0, maxPrintCharacters - 1)) - /** Return a String representation of a value we got from `classLoader()`. */ - private[repl] def replStringOf(value: Object, prefixLength: Int)(using Context): String = { + /** Return a colored fansi.Str representation of a value we got from `classLoader()`. */ + private[repl] def replStringOf(value: Object, prefixLength: Int)(using Context): fansi.Str = { // pretty-print things with 100 cols 50 rows by default, pprintRender( value, @@ -100,7 +106,7 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): * * Calling this method evaluates the expression using reflection */ - private def valueOf(sym: Symbol, prefixLength: Int)(using Context): Option[String] = + private def valueOf(sym: Symbol, prefixLength: Int)(using Context): Option[fansi.Str] = val objectName = sym.owner.fullName.encode.toString.stripSuffix("$") val resObj: Class[?] = Class.forName(objectName, true, classLoader()) val symValue = resObj @@ -109,11 +115,14 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): .flatMap(result => rewrapValueClass(sym.info.classSymbol, result.invoke(null))) symValue .filter(_ => sym.is(Flags.Method) || sym.info != defn.UnitType) - .map(value => stripReplPrefix(replStringOf(value, prefixLength))) - - private def stripReplPrefix(s: String): String = - if (s.startsWith(REPL_WRAPPER_NAME_PREFIX)) - s.drop(REPL_WRAPPER_NAME_PREFIX.length).dropWhile(c => c.isDigit || c == '$') + .map(value => stripReplPrefixFansi(replStringOf(value, prefixLength))) + + private def stripReplPrefixFansi(s: fansi.Str): fansi.Str = + val plain = s.plainText + if (plain.startsWith(REPL_WRAPPER_NAME_PREFIX)) + val prefixLen = REPL_WRAPPER_NAME_PREFIX.length + val dropLen = prefixLen + plain.drop(prefixLen).takeWhile(c => c.isDigit || c == '$').length + s.substring(dropLen) else s @@ -141,17 +150,17 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): /** Render value definition result */ def renderVal(d: Denotation)(using Context): Either[ReflectiveOperationException, Option[Diagnostic]] = - val dcl = d.symbol.showUser - def msg(s: String) = infoDiagnostic(s, d) + val dcl = fansi.Str(SyntaxHighlighting.highlight(d.symbol.showUser)) + def msg(s: fansi.Str) = infoDiagnostic(s, d) try Right( if d.symbol.is(Flags.Lazy) then Some(msg(dcl)) else { - val prefix = s"$dcl = " + val prefix = dcl ++ " = " // Prefix can have multiple lines, only consider the last one // when determining the initial column offset for pretty-printing - val prefixLength = prefix.linesIterator.toSeq.lastOption.getOrElse("").length - valueOf(d.symbol, prefixLength).map(value => msg(s"$prefix$value")) + val prefixLength = prefix.plainText.linesIterator.toSeq.lastOption.getOrElse("").length + valueOf(d.symbol, prefixLength).map(value => msg(prefix ++ value)) } ) catch case e: ReflectiveOperationException => Left(e) @@ -181,8 +190,12 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): infoDiagnostic(cause.formatStackTracePrefix(!isWrapperInitialization(_)), d) end renderError + private def infoDiagnostic(msg: fansi.Str, d: Denotation)(using Context): Diagnostic = { + new Diagnostic.Info(msg.render, d.symbol.sourcePos) + } + private def infoDiagnostic(msg: String, d: Denotation)(using Context): Diagnostic = - new Diagnostic.Info(msg, d.symbol.sourcePos) + new Diagnostic.Info(SyntaxHighlighting.highlight(msg), d.symbol.sourcePos) object Rendering: final val REPL_WRAPPER_NAME_PREFIX = str.REPL_SESSION_LINE diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 5cd8fd0bf539..3ad98b045177 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -497,9 +497,7 @@ class ReplDriver(settings: Array[String], val formattedTypeDefs = // don't render type defs if wrapper initialization failed if newState.invalidObjectIndexes.contains(state.objectIndex) then Seq.empty else typeDefs(wrapperModule.symbol) - val highlighted = (formattedTypeDefs ++ formattedMembers) - .map(d => new Diagnostic(d.msg.mapMsg(SyntaxHighlighting.highlight), d.pos, d.level)) - (newState, highlighted) + (newState, formattedTypeDefs ++ formattedMembers) } .getOrElse { // user defined a trait/class/object, so no module needed From 3a910e57074b93815aae709dfea69e16f1020d95 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 29 Oct 2025 11:07:43 +0800 Subject: [PATCH 2/5] . --- compiler/src/dotty/tools/repl/Rendering.scala | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/compiler/src/dotty/tools/repl/Rendering.scala b/compiler/src/dotty/tools/repl/Rendering.scala index e0e57d4e0535..e83bf7ce00d5 100644 --- a/compiler/src/dotty/tools/repl/Rendering.scala +++ b/compiler/src/dotty/tools/repl/Rendering.scala @@ -28,21 +28,18 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): var myClassLoader: AbstractFileClassLoader = uninitialized - private def pprintRender(value: Any, width: Int, height: Int, initialOffset: Int)(using Context): fansi.Str = { - def fallback() = { - - val fansiStr = pprint.PPrinter.Color + private def pprintRender(value: Any, width: Int, height: Int, initialOffset: Int)(using Context): String = { + def fallback() = + dotty.shaded.pprint.PPrinter.Color .apply(value, width = width, height = height, initialOffset = initialOffset) - dotty.shaded.pprint.log(fansiStr.toString.toCharArray) - fansiStr - } - + .render + .toString try val cl = classLoader() - val pprintCls = Class.forName("pprint.PPrinter$Color$", false, cl) - val fansiStrCls = Class.forName("fansi.Str", false, cl) - val Color = pprintCls.getField("MODULE$").get(null) - val Color_apply = pprintCls.getMethod("apply", + val pprintColorCls = Class.forName("dotty.shaded.pprint.PPrinter$Color$", false, cl) + val fansiStrCls = Class.forName("dotty.shaded.fansi.Str", false, cl) + val Color = pprintColorCls.getField("MODULE$").get(null) + val Color_apply = pprintColorCls.getMethod("apply", classOf[Any], // value classOf[Int], // width classOf[Int], // height @@ -51,11 +48,11 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): classOf[Boolean], // escape Unicode classOf[Boolean], // show field names ) + val FansiStr_render = fansiStrCls.getMethod("render") val fansiStr = Color_apply.invoke( Color, value, width, height, 2, initialOffset, false, true ) - dotty.shaded.pprint.log(fansiStr.toString.toCharArray) - fansi.Str(fansiStr.toString) + FansiStr_render.invoke(fansiStr).asInstanceOf[String] catch case _: ClassNotFoundException => fallback() case _: NoSuchMethodException => fallback() From b182b3dc3084fbe1c1fe5d6b678837a221431283 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 29 Oct 2025 11:23:26 +0800 Subject: [PATCH 3/5] . --- compiler/src/dotty/tools/dotc/util/StackTraceOps.scala | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala index 03fca0d05b3a..5ed456a2bdf9 100644 --- a/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala +++ b/compiler/src/dotty/tools/dotc/util/StackTraceOps.scala @@ -37,6 +37,9 @@ object StackTraceOps: val Self = "" val CausedBy = "Caused by: " val Suppressed = "Suppressed: " + def renderDotDelimited(s: String ) = { + fansi.Str.join(s.split('.').map(fansi.Color.LightRed(_)), ".") + } def header(e: Throwable): fansi.Str = def because = e.getCause match { case null => null ; case c => header(c) } @@ -45,13 +48,12 @@ object StackTraceOps: case null => "" case s => s": $s" } - fansi.Color.LightRed(e.getClass.getName) ++ txt + renderDotDelimited(e.getClass.getName) ++ txt val seen = mutable.Set.empty[Throwable] def unseen(e: Throwable): Boolean = (e != null && !seen(e)).tap(if _ then seen += e) val lines = ListBuffer.empty[fansi.Str] - // format the stack trace, skipping the shared trace def print(e: Throwable, r: TraceRelation, share: Array[StackTraceElement], indents: Int): Unit = if unseen(e) then val trace = e.getStackTrace @@ -65,7 +67,7 @@ object StackTraceOps: prefix.foreach(frame => lines += fansi.Str(s"$margin ") ++ fansi.Color.Red("at") ++ " " ++ - fansi.Str.join(frame.getClassName.split('.').map(fansi.Color.LightRed(_)), ".") ++ "." ++ + renderDotDelimited(frame.getClassName) ++ "." ++ fansi.Color.LightRed(frame.getMethodName) ++ "(" ++ fansi.Color.LightRed(frame.getFileName) ++ ":" ++ fansi.Color.LightRed(frame.getLineNumber.toString) ++ ")" From f83a1b1df88b0e25c943b3b54d2aa0e7e3671687 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 29 Oct 2025 11:52:18 +0800 Subject: [PATCH 4/5] . --- compiler/src/dotty/tools/repl/Rendering.scala | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/src/dotty/tools/repl/Rendering.scala b/compiler/src/dotty/tools/repl/Rendering.scala index e83bf7ce00d5..c581449b6db6 100644 --- a/compiler/src/dotty/tools/repl/Rendering.scala +++ b/compiler/src/dotty/tools/repl/Rendering.scala @@ -91,12 +91,13 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): /** Return a colored fansi.Str representation of a value we got from `classLoader()`. */ private[repl] def replStringOf(value: Object, prefixLength: Int)(using Context): fansi.Str = { // pretty-print things with 100 cols 50 rows by default, - pprintRender( + val res = pprintRender( value, width = 100, height = 50, initialOffset = prefixLength ) + if (ctx.settings.color.value == "never") fansi.Str(res).plainText else res } /** Load the value of the symbol using reflection. @@ -184,7 +185,9 @@ private[repl] class Rendering(parentClassLoader: Option[ClassLoader] = None): ste.getClassName.startsWith(REPL_WRAPPER_NAME_PREFIX) // d.symbol.owner.name.show is simple name && (ste.getMethodName == nme.STATIC_CONSTRUCTOR.show || ste.getMethodName == nme.CONSTRUCTOR.show) - infoDiagnostic(cause.formatStackTracePrefix(!isWrapperInitialization(_)), d) + val formatted0 = cause.formatStackTracePrefix(!isWrapperInitialization(_)) + val formatted: fansi.Str = if (ctx.settings.color.value == "never") formatted0.plainText else formatted0 + infoDiagnostic(formatted, d) end renderError private def infoDiagnostic(msg: fansi.Str, d: Denotation)(using Context): Diagnostic = { From bd06199b061b9cc6fb01f6c58bda1106b45e9557 Mon Sep 17 00:00:00 2001 From: Li Haoyi Date: Wed, 29 Oct 2025 12:57:26 +0800 Subject: [PATCH 5/5] . --- compiler/test/dotty/tools/dotc/util/StackTraceTest.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler/test/dotty/tools/dotc/util/StackTraceTest.scala b/compiler/test/dotty/tools/dotc/util/StackTraceTest.scala index 7e49fd64c146..5d16e86eddbd 100644 --- a/compiler/test/dotty/tools/dotc/util/StackTraceTest.scala +++ b/compiler/test/dotty/tools/dotc/util/StackTraceTest.scala @@ -37,7 +37,7 @@ class StackTraceTest: // evaluating s should throw, p trims stack trace, t is the test of resulting trace string def probe(s: => String)(p: StackTraceElement => Boolean)(t: String => Unit): Unit = import StackTraceOps.formatStackTracePrefix - Try(s).recover { case e => e.formatStackTracePrefix(p) } match + Try(s).recover { case e => e.formatStackTracePrefix(p).plainText } match case Success(s) => t(s) case Failure(e) => throw e