Add colours to Pkl errors in Cli output

To make error messages from Pkl eval easier to read, this change uses
the Jansi library to colour the output, making it quicker and easier to
scan error messages and understand what's happened.

The Jansi library also detects if the CLI output is a terminal capable
of handling colours, and will automatically strip out escape codes if
the output won't support them (e.g. piping the output somewhere else).
This commit is contained in:
Thomas Purchas
2024-06-26 23:15:05 +01:00
committed by Philip K.F. Hölzenspies
parent 49aaf288cc
commit 0d7b95d3ff
27 changed files with 93 additions and 28 deletions

View File

@@ -15,15 +15,23 @@
*/
package org.pkl.core.runtime;
import static org.fusesource.jansi.Ansi.ansi;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.Ansi.Color;
import org.pkl.core.StackFrame;
import org.pkl.core.util.Nullable;
public final class StackTraceRenderer {
private final Function<StackFrame, StackFrame> frameTransformer;
private static final Ansi.Color frameColor = Color.YELLOW;
private static final Ansi.Color lineNumColor = Color.BLUE;
private static final Ansi.Color repetitionColor = Color.MAGENTA;
public StackTraceRenderer(Function<StackFrame, StackFrame> frameTransformer) {
this.frameTransformer = frameTransformer;
}
@@ -40,6 +48,7 @@ public final class StackTraceRenderer {
StringBuilder builder,
String leftMargin,
boolean isFirstElement) {
var out = ansi(builder);
for (var frame : frames) {
if (frame instanceof StackFrameLoop loop) {
// ensure a cycle of length 1 doesn't get rendered as a loop
@@ -47,20 +56,28 @@ public final class StackTraceRenderer {
doRender(loop.frames, null, builder, leftMargin, isFirstElement);
} else {
if (!isFirstElement) {
builder.append(leftMargin).append("\n");
out.fgBright(frameColor).a(leftMargin).reset().a("\n");
}
builder.append(leftMargin).append("┌─ ").append(loop.count).append(" repetitions of:\n");
out.fgBright(frameColor)
.a(leftMargin)
.a("┌─ ")
.reset()
.bold()
.fg(repetitionColor)
.a(Integer.toString(loop.count))
.reset()
.a(" repetitions of:\n");
var newLeftMargin = leftMargin + "";
doRender(loop.frames, null, builder, newLeftMargin, isFirstElement);
if (isFirstElement) {
renderHint(hint, builder, newLeftMargin);
isFirstElement = false;
}
builder.append(leftMargin).append("└─\n");
out.fgBright(frameColor).a(leftMargin).a("└─").reset().a("\n");
}
} else {
if (!isFirstElement) {
builder.append(leftMargin).append('\n');
out.fgBright(frameColor).a(leftMargin).reset().a('\n');
}
renderFrame((StackFrame) frame, builder, leftMargin);
}
@@ -80,14 +97,16 @@ public final class StackTraceRenderer {
private void renderHint(@Nullable String hint, StringBuilder builder, String leftMargin) {
if (hint == null || hint.isEmpty()) return;
var out = ansi(builder);
builder.append('\n');
builder.append(leftMargin);
builder.append(hint);
builder.append('\n');
out.a('\n');
out.fgBright(frameColor).a(leftMargin);
out.fgBright(frameColor).bold().a(hint).reset();
out.a('\n');
}
private void renderSourceLine(StackFrame frame, StringBuilder builder, String leftMargin) {
var out = ansi(builder);
var originalSourceLine = frame.getSourceLines().get(0);
var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine);
var sourceLine = originalSourceLine.strip();
@@ -98,27 +117,36 @@ public final class StackTraceRenderer {
: sourceLine.length();
var prefix = frame.getStartLine() + " | ";
builder.append(leftMargin).append(prefix).append(sourceLine).append('\n');
builder.append(leftMargin);
out.fgBright(frameColor)
.a(leftMargin)
.fgBright(lineNumColor)
.a(prefix)
.reset()
.a(sourceLine)
.a('\n');
out.fgBright(frameColor).a(leftMargin).reset();
//noinspection StringRepeatCanBeUsed
for (int i = 1; i < prefix.length() + startColumn; i++) {
builder.append(' ');
out.append(' ');
}
out.fgRed();
//noinspection StringRepeatCanBeUsed
for (int i = startColumn; i <= endColumn; i++) {
builder.append('^');
out.a('^');
}
builder.append('\n');
out.reset().a('\n');
}
private void renderSourceLocation(StackFrame frame, StringBuilder builder, String leftMargin) {
builder.append(leftMargin).append("at ");
var out = ansi(builder);
out.fgBright(frameColor).a(leftMargin).reset().a("at ");
if (frame.getMemberName() != null) {
builder.append(frame.getMemberName());
out.a(frame.getMemberName());
} else {
builder.append("<unknown>");
out.a("<unknown>");
}
builder.append(" (").append(frame.getModuleUri()).append(')').append('\n');
out.a(" (").a(frame.getModuleUri()).a(')').a('\n');
}
/**

View File

@@ -15,10 +15,14 @@
*/
package org.pkl.core.runtime;
import static org.fusesource.jansi.Ansi.ansi;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import org.fusesource.jansi.Ansi;
import org.fusesource.jansi.Ansi.Color;
import org.pkl.core.Release;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable;
@@ -27,6 +31,8 @@ import org.pkl.core.util.StringBuilderWriter;
public final class VmExceptionRenderer {
private final @Nullable StackTraceRenderer stackTraceRenderer;
private static final Ansi.Color errorColor = Color.RED;
/**
* Constructs an error renderer with the given stack trace renderer. If stack trace renderer is
* {@code null}, stack traces will not be included in error output.
@@ -73,7 +79,8 @@ public final class VmExceptionRenderer {
}
private void renderException(VmException exception, StringBuilder builder) {
var header = " Pkl Error ";
var out = ansi(builder);
out.fg(errorColor).a(" Pkl Error ").reset();
String message;
var hint = exception.getHint();
@@ -94,7 +101,7 @@ public final class VmExceptionRenderer {
message = exception.getMessage();
}
builder.append(header).append('\n').append(message).append('\n');
out.a('\n').fgBright(errorColor).a(message).reset().a('\n');
// include cause's message unless it's the same as this exception's message
if (exception.getCause() != null) {

View File

@@ -3,7 +3,6 @@ package org.pkl.core
import org.pkl.commons.createTempFile
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.pkl.commons.writeString
@@ -11,7 +10,6 @@ import org.pkl.core.ModuleSource.*
import java.nio.file.Files
import java.nio.file.Path
import kotlin.io.path.createFile
import kotlin.io.path.name
class EvaluateTestsTest {

View File

@@ -429,7 +429,7 @@ class EvaluatorTest {
val evaluatorBuilder = EvaluatorBuilder.preconfigured().setModuleCacheDir(cacheDir)
val project = Project.load(modulePath("/org/pkl/core/project/project6/PklProject"))
evaluatorBuilder.setProjectDependencies(project.dependencies).build().use { evaluator ->
assertThatCode {
assertThatCode {
evaluator.evaluateOutputText(modulePath("/org/pkl/core/project/project6/globWithinDependency.pkl"))
}.hasMessageContaining("""
Cannot resolve import in local dependency because scheme `modulepath` is not globbable.

View File

@@ -149,8 +149,9 @@ class ProjectTest {
.setModuleCacheDir(null)
.setHttpClient(httpClient)
.build()
assertThatCode { evaluator.evaluate(ModuleSource.path(projectDir.resolve("bug.pkl"))) }
.hasMessageStartingWith("""
assertThatCode {
evaluator.evaluate(ModuleSource.path(projectDir.resolve("bug.pkl")))
}.hasMessageStartingWith("""
Pkl Error
Cannot download package `package://localhost:0/fruit@1.0.5` because the computed checksum for package metadata does not match the expected checksum.