diff --git a/core/src/main/scala/chester/repl/REPLEngine.scala b/core/src/main/scala/chester/repl/REPLEngine.scala index 2ec0ebb9..8570307d 100644 --- a/core/src/main/scala/chester/repl/REPLEngine.scala +++ b/core/src/main/scala/chester/repl/REPLEngine.scala @@ -3,7 +3,7 @@ package chester.repl import cats.implicits.* import chester.doc.* import chester.doc.const.{Colors, ReplaceBracketsWithWord} -import chester.error.unreachable +import chester.error.* import chester.reader.{ParseError, ReaderREPL} import chester.syntax.concrete.Expr import chester.syntax.core.* @@ -145,15 +145,17 @@ def REPLEngine[F[_]](using er: Vector[chester.error.TyckError], wr: Vector[chester.error.TyckWarning] = Vector() ): F[Unit] = { + given sourceReader: SourceReader = SourceReader.default + for { _ <- er.traverse(x => { InTerminal.writeln( - FansiPrettyPrinter.render(x.renderWithLocation, maxWidth) + FansiPrettyPrinter.render(x.renderDoc, maxWidth) ) }) _ <- wr.traverse(x => { InTerminal.writeln( - FansiPrettyPrinter.render(x.renderWithLocation, maxWidth) + FansiPrettyPrinter.render(x.renderDoc, maxWidth) ) }) } yield () diff --git a/err/src/main/scala/chester/error/TyckProblem.scala b/err/src/main/scala/chester/error/TyckProblem.scala index c571812a..0508d1d4 100644 --- a/err/src/main/scala/chester/error/TyckProblem.scala +++ b/err/src/main/scala/chester/error/TyckProblem.scala @@ -21,9 +21,7 @@ sealed trait TyckProblem extends Problem derives ReadWriter { def hint: ToDoc = empty - override def toDoc(using - options: PrettierOptions - ): Doc + override def toDoc(using options: PrettierOptions): Doc def cause: Term | Expr @@ -31,39 +29,6 @@ sealed trait TyckProblem extends Problem derives ReadWriter { case x: WithPos => x.sourcePos case _ => None } - - def renderWithLocation(using - options: PrettierOptions - ): Doc = { - val baseMessage = Doc.text(t"Error") <+> this - - val locationInfo = sourcePos match { - case Some(pos) => - val lines = pos.getLinesInRange match { - case Some(lines) => - lines.map { case (lineNumber, line) => - Doc.text(t"$lineNumber") <+> Doc.text(line, Styling.BoldOn) - } - case None => Vector.empty - } - val locationHeader = Doc.text(t"Location") <+> - Doc.text( - t"${pos.fileName} [${pos.range.start.line + 1}:${pos.range.start.column.i + 1}] to [${pos.range.end.line + 1}:${pos.range.end.column.i + 1}]", - Styling.BoldOn - ) - - val codeBlock = Doc.group(Doc.concat(lines.map(_.end)*)) - - locationHeader <|> codeBlock - - case None => - val causeHeader = Doc.text(t"Cause", Styling.BoldOn) - val causeText = cause - causeHeader <|> causeText - } - - baseMessage <|> locationInfo - } } sealed trait TyckError extends TyckProblem derives ReadWriter { diff --git a/syntax/shared/src/main/scala/chester/error/Problem.scala b/syntax/shared/src/main/scala/chester/error/Problem.scala index 6d6d28c4..ed37ff5c 100644 --- a/syntax/shared/src/main/scala/chester/error/Problem.scala +++ b/syntax/shared/src/main/scala/chester/error/Problem.scala @@ -2,6 +2,7 @@ package chester.error import chester.utils.doc.* import upickle.default.* +import chester.i18n.* object Problem { enum Stage derives ReadWriter { @@ -66,7 +67,12 @@ private def renderFullDescription(desc: FullDescription)(using options: Prettier val explanationsDoc = desc.explanations.map { elem => val elemDoc = elem.doc.toDoc elem.sourcePos.flatMap(sourceReader.apply) match { - case Some(source) => elemDoc Doc.text(source) + case Some(lines) => + val sourceLines = lines.map { case (lineNumber, line) => + Doc.text(t"$lineNumber") <+> Doc.text(line, Styling.BoldOn) + } + val codeBlock = Doc.group(Doc.concat(sourceLines.map(_.end)*)) + elemDoc codeBlock case None => elemDoc } } @@ -82,27 +88,55 @@ private def renderFullDescription(desc: FullDescription)(using options: Prettier } private def renderToDocWithSource(p: Problem)(using options: PrettierOptions, sourceReader: SourceReader): Doc = { - val baseDoc = p.toDoc - p.sourcePos.flatMap(sourceReader.apply) match { - case Some(source) => - baseDoc <> Doc.line <> Doc.text(source) + val severityDoc = p.severity match { + case Problem.Severity.Error => Doc.text(t"Error") + case Problem.Severity.Warning => Doc.text(t"Warning") + case Problem.Severity.Goal => Doc.text(t"Goal") + case Problem.Severity.Info => Doc.text(t"Info") + } + + val baseDoc = severityDoc <+> p.toDoc + + p.sourcePos match { + case Some(pos) => + val locationHeader = Doc.text(t"Location") <+> + Doc.text( + t"${pos.fileName} [${pos.range.start.line + 1}:${pos.range.start.column.i + 1}] to [${pos.range.end.line + 1}:${pos.range.end.column.i + 1}]", + Styling.BoldOn + ) + + val sourceLines = sourceReader(pos).map { lines => + lines.map { case (lineNumber, line) => + Doc.text(t"$lineNumber") <+> Doc.text(line, Styling.BoldOn) + } + }.getOrElse(Vector.empty) + + val codeBlock = Doc.group(Doc.concat(sourceLines.map(_.end)*)) + + baseDoc <|> locationHeader <|> codeBlock + case None => baseDoc } } - -case class SourceReader(readSource: SourcePos => Option[String]) { - def apply(pos: SourcePos): Option[String] = readSource(pos) +/** A reader for source code that provides line-numbered content. + * + * @param readSource A function that takes a SourcePos and returns line-numbered content. + * The returned Vector contains tuples of (lineNumber, lineContent) where: + * - lineNumber: 1-based line numbers (e.g., lines 3,4,5) + * - lineContent: The actual text content of that line + * Note: While internal line tracking is 0-based, this API returns 1-based line numbers for display + */ +case class SourceReader(readSource: SourcePos => Option[Vector[(Int, String)]]) { + def apply(pos: SourcePos): Option[Vector[(Int, String)]] = readSource(pos) } object SourceReader { def fromFileContent(content: FileContent): SourceReader = { - SourceReader { pos => - pos.getLinesInRange.map { lines => - lines.map { case (_, line) => line }.mkString("\n") - } - } + SourceReader { pos => pos.getLinesInRange } } + def default: SourceReader = SourceReader { pos => pos.getLinesInRange } + def empty: SourceReader = SourceReader(_ => None) } diff --git a/syntax/shared/src/main/scala/chester/error/SourcePos.scala b/syntax/shared/src/main/scala/chester/error/SourcePos.scala index edd96dbf..386a462e 100644 --- a/syntax/shared/src/main/scala/chester/error/SourcePos.scala +++ b/syntax/shared/src/main/scala/chester/error/SourcePos.scala @@ -48,7 +48,14 @@ case class SourcePos(source: SourceOffset, range: RangeInFile) derives ReadWrite ) val fileName = source.fileName - // Method to extract all lines within the range with line numbers + /** Extracts all lines within the range with their line numbers. + * + * @return Option containing a Vector of (lineNumber, lineContent) tuples where: + * - lineNumber: 1-based line numbers for display (e.g., if range spans lines 3-5, + * returns exactly [(3,"line3"), (4,"line4"), (5,"line5")]) + * - lineContent: The actual text content of that line + * Note: While internal line tracking is 0-based, this API returns 1-based line numbers for display + */ def getLinesInRange: Option[Vector[(Int, String)]] = fileContent map { fileContent => val startLine = range.start.line - fileContent.lineOffset val endLine = range.end.line - fileContent.lineOffset