mirror of
https://github.com/apple/pkl.git
synced 2026-04-22 08:18:32 +02:00
Add syntax highlighting of Pkl code (#1385)
This adds syntax highlighting of Pkl code! It adds highlighting for: * Stack frames within error messages * CLI REPL (highlights as you type, highlights error output) * Power assertions (coming in https://github.com/apple/pkl/pull/1384) This uses the lexer for highlighting. It will highlight strings, numbers, keywords, but doesn't understand how to highlight nodes like types, function params, etc. The reason for this is because a single line of code by itself may not be grammatically valid.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -45,10 +45,12 @@ import org.pkl.core.repl.ReplResponse.EvalSuccess;
|
||||
import org.pkl.core.repl.ReplResponse.InvalidRequest;
|
||||
import org.pkl.core.resource.ResourceReader;
|
||||
import org.pkl.core.runtime.*;
|
||||
import org.pkl.core.util.AnsiStringBuilder;
|
||||
import org.pkl.core.util.EconomicMaps;
|
||||
import org.pkl.core.util.IoUtils;
|
||||
import org.pkl.core.util.MutableReference;
|
||||
import org.pkl.core.util.Nullable;
|
||||
import org.pkl.core.util.SyntaxHighlighter;
|
||||
import org.pkl.parser.Parser;
|
||||
import org.pkl.parser.ParserError;
|
||||
import org.pkl.parser.syntax.Class;
|
||||
@@ -69,6 +71,7 @@ public class ReplServer implements AutoCloseable {
|
||||
private final VmExceptionRenderer errorRenderer;
|
||||
private final PackageResolver packageResolver;
|
||||
private final @Nullable ProjectDependenciesManager projectDependenciesManager;
|
||||
private final boolean color;
|
||||
|
||||
public ReplServer(
|
||||
SecurityManager securityManager,
|
||||
@@ -90,6 +93,7 @@ public class ReplServer implements AutoCloseable {
|
||||
this.securityManager = securityManager;
|
||||
this.moduleResolver = new ModuleResolver(moduleKeyFactories);
|
||||
this.errorRenderer = new VmExceptionRenderer(new StackTraceRenderer(frameTransformer), color);
|
||||
this.color = color;
|
||||
replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype()));
|
||||
|
||||
var languageRef = new MutableReference<VmLanguage>(null);
|
||||
@@ -172,7 +176,7 @@ public class ReplServer implements AutoCloseable {
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@SuppressWarnings({"StatementWithEmptyBody", "DataFlowIssue"})
|
||||
@SuppressWarnings({"StatementWithEmptyBody"})
|
||||
private List<Object> evaluate(
|
||||
ReplState replState,
|
||||
String requestId,
|
||||
@@ -448,7 +452,10 @@ public class ReplServer implements AutoCloseable {
|
||||
}
|
||||
|
||||
private String render(Object value) {
|
||||
return VmValueRenderer.multiLine(Integer.MAX_VALUE).render(value);
|
||||
var sb = new AnsiStringBuilder(color);
|
||||
var src = VmValueRenderer.multiLine(Integer.MAX_VALUE).render(value);
|
||||
SyntaxHighlighter.writeTo(sb, src);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
private static class ReplState {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import org.pkl.core.StackFrame;
|
||||
import org.pkl.core.util.AnsiStringBuilder;
|
||||
import org.pkl.core.util.AnsiTheme;
|
||||
import org.pkl.core.util.Nullable;
|
||||
import org.pkl.core.util.SyntaxHighlighter;
|
||||
|
||||
public final class StackTraceRenderer {
|
||||
private final Function<StackFrame, StackFrame> frameTransformer;
|
||||
@@ -104,9 +105,11 @@ public final class StackTraceRenderer {
|
||||
|
||||
var prefix = frame.getStartLine() + " | ";
|
||||
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
|
||||
.append(AnsiTheme.STACK_TRACE_LINE_NUMBER, prefix)
|
||||
.append(sourceLine)
|
||||
.append('\n')
|
||||
.append(AnsiTheme.STACK_TRACE_LINE_NUMBER, prefix);
|
||||
|
||||
SyntaxHighlighter.writeTo(out, sourceLine);
|
||||
|
||||
out.append('\n')
|
||||
.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
|
||||
.append(" ".repeat(prefix.length() + startColumn - 1))
|
||||
.append(AnsiTheme.STACK_TRACE_CARET, "^".repeat(endColumn - startColumn + 1))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,7 +19,7 @@ import java.io.PrintWriter;
|
||||
import java.util.EnumSet;
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
@SuppressWarnings({"DuplicatedCode", "UnusedReturnValue"})
|
||||
public final class AnsiStringBuilder {
|
||||
private final StringBuilder builder = new StringBuilder();
|
||||
private final boolean usingColor;
|
||||
@@ -108,6 +108,25 @@ public final class AnsiStringBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/** Provides a runnable where anything appended is not affected by the existing context. */
|
||||
public AnsiStringBuilder appendSandboxed(Runnable runnable) {
|
||||
if (!usingColor) {
|
||||
runnable.run();
|
||||
return this;
|
||||
}
|
||||
var myCodes = currentCodes;
|
||||
var myDeclaredCodes = declaredCodes;
|
||||
currentCodes = EnumSet.noneOf(AnsiCode.class);
|
||||
declaredCodes = EnumSet.noneOf(AnsiCode.class);
|
||||
doReset();
|
||||
runnable.run();
|
||||
doReset();
|
||||
currentCodes = myCodes;
|
||||
declaredCodes = myDeclaredCodes;
|
||||
doAppendCodes(currentCodes);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append a string whose contents are unknown, and might contain ANSI color codes.
|
||||
*
|
||||
@@ -180,6 +199,14 @@ public final class AnsiStringBuilder {
|
||||
return new PrintWriter(new StringBuilderWriter(builder));
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return builder.length();
|
||||
}
|
||||
|
||||
public void setLength(int length) {
|
||||
builder.setLength(length);
|
||||
}
|
||||
|
||||
/** Builds the data represented by this builder into a {@link String}. */
|
||||
public String toString() {
|
||||
// be a good citizen and unset any ansi escape codes currently set.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
* Copyright © 2024-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -37,5 +37,15 @@ public final class AnsiTheme {
|
||||
public static final AnsiCode TEST_NAME = AnsiCode.FAINT;
|
||||
public static final AnsiCode TEST_FACT_SOURCE = AnsiCode.RED;
|
||||
public static final AnsiCode TEST_FAILURE_MESSAGE = AnsiCode.RED;
|
||||
public static final Set<AnsiCode> TEST_EXAMPLE_OUTPUT = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD);
|
||||
public static final EnumSet<AnsiCode> TEST_EXAMPLE_OUTPUT =
|
||||
EnumSet.of(AnsiCode.RED, AnsiCode.BOLD);
|
||||
|
||||
public static final AnsiCode SYNTAX_KEYWORD = AnsiCode.BLUE;
|
||||
public static final AnsiCode SYNTAX_NUMBER = AnsiCode.GREEN;
|
||||
public static final AnsiCode SYNTAX_STRING = AnsiCode.YELLOW;
|
||||
public static final AnsiCode SYNTAX_STRING_ESCAPE = AnsiCode.BRIGHT_YELLOW;
|
||||
public static final AnsiCode SYNTAX_COMMENT = AnsiCode.FAINT;
|
||||
public static final AnsiCode SYNTAX_OPERATOR = AnsiCode.RESET;
|
||||
public static final AnsiCode SYNTAX_CONTROL = AnsiCode.BLUE;
|
||||
public static final AnsiCode SYNTAX_CONSTANT = AnsiCode.CYAN;
|
||||
}
|
||||
|
||||
172
pkl-core/src/main/java/org/pkl/core/util/SyntaxHighlighter.java
Normal file
172
pkl-core/src/main/java/org/pkl/core/util/SyntaxHighlighter.java
Normal file
@@ -0,0 +1,172 @@
|
||||
/*
|
||||
* Copyright © 2025-2026 Apple Inc. and the Pkl project authors. All rights reserved.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.pkl.core.util;
|
||||
|
||||
import static org.pkl.parser.Token.FALSE;
|
||||
import static org.pkl.parser.Token.NULL;
|
||||
import static org.pkl.parser.Token.STRING_ESCAPE_BACKSLASH;
|
||||
import static org.pkl.parser.Token.STRING_ESCAPE_NEWLINE;
|
||||
import static org.pkl.parser.Token.STRING_ESCAPE_QUOTE;
|
||||
import static org.pkl.parser.Token.STRING_ESCAPE_RETURN;
|
||||
import static org.pkl.parser.Token.STRING_ESCAPE_TAB;
|
||||
import static org.pkl.parser.Token.STRING_ESCAPE_UNICODE;
|
||||
import static org.pkl.parser.Token.TRUE;
|
||||
|
||||
import java.util.EnumSet;
|
||||
import org.pkl.parser.Lexer;
|
||||
import org.pkl.parser.ParserError;
|
||||
import org.pkl.parser.Token;
|
||||
|
||||
/** Syntax highlighter that emits ansi color codes. */
|
||||
public final class SyntaxHighlighter {
|
||||
private SyntaxHighlighter() {}
|
||||
|
||||
private static final EnumSet<Token> stringEscape =
|
||||
EnumSet.of(
|
||||
STRING_ESCAPE_NEWLINE,
|
||||
STRING_ESCAPE_TAB,
|
||||
STRING_ESCAPE_RETURN,
|
||||
STRING_ESCAPE_QUOTE,
|
||||
STRING_ESCAPE_BACKSLASH,
|
||||
STRING_ESCAPE_UNICODE);
|
||||
|
||||
private static final EnumSet<Token> constant = EnumSet.of(TRUE, FALSE, NULL);
|
||||
|
||||
private static final EnumSet<Token> operator =
|
||||
EnumSet.of(
|
||||
Token.COALESCE,
|
||||
Token.LT,
|
||||
Token.GT,
|
||||
Token.NOT,
|
||||
Token.EQUAL,
|
||||
Token.NOT_EQUAL,
|
||||
Token.LTE,
|
||||
Token.GTE,
|
||||
Token.AND,
|
||||
Token.OR,
|
||||
Token.PLUS,
|
||||
Token.MINUS,
|
||||
Token.POW,
|
||||
Token.STAR,
|
||||
Token.DIV,
|
||||
Token.INT_DIV,
|
||||
Token.MOD,
|
||||
Token.PIPE);
|
||||
|
||||
private static final EnumSet<Token> keyword =
|
||||
EnumSet.of(
|
||||
Token.AMENDS,
|
||||
Token.AS,
|
||||
Token.EXTENDS,
|
||||
Token.CLASS,
|
||||
Token.TYPE_ALIAS,
|
||||
Token.FUNCTION,
|
||||
Token.MODULE,
|
||||
Token.IMPORT,
|
||||
Token.IMPORT_STAR,
|
||||
Token.READ,
|
||||
Token.READ_STAR,
|
||||
Token.READ_QUESTION,
|
||||
Token.TRACE,
|
||||
Token.THROW,
|
||||
Token.UNKNOWN,
|
||||
Token.NOTHING,
|
||||
Token.OUTER,
|
||||
Token.SUPER,
|
||||
Token.THIS,
|
||||
Token.HIDDEN,
|
||||
Token.ABSTRACT,
|
||||
Token.CONST,
|
||||
Token.FIXED,
|
||||
Token.LOCAL,
|
||||
Token.OPEN);
|
||||
|
||||
private static final EnumSet<Token> control =
|
||||
EnumSet.of(Token.NEW, Token.IF, Token.ELSE, Token.WHEN, Token.FOR, Token.IN, Token.OUT);
|
||||
|
||||
private static final EnumSet<Token> number =
|
||||
EnumSet.of(Token.INT, Token.FLOAT, Token.BIN, Token.OCT, Token.HEX);
|
||||
|
||||
public static void writeTo(AnsiStringBuilder out, String src) {
|
||||
var prevLength = out.length();
|
||||
try {
|
||||
var lexer = new Lexer(src);
|
||||
doHighlightNormal(out, lexer.next(), lexer, Token.EOF);
|
||||
} catch (ParserError err) {
|
||||
// bail out and emit everything un-highlighted
|
||||
out.setLength(prevLength);
|
||||
out.append(src);
|
||||
}
|
||||
}
|
||||
|
||||
private static void highlightString(AnsiStringBuilder out, Lexer lexer, Token token) {
|
||||
out.append(
|
||||
AnsiTheme.SYNTAX_STRING,
|
||||
() -> {
|
||||
var next = token;
|
||||
while (next != Token.STRING_END && next != Token.EOF) {
|
||||
if (stringEscape.contains(next)) {
|
||||
out.append(AnsiTheme.SYNTAX_STRING_ESCAPE, lexer.text());
|
||||
next = advance(out, lexer);
|
||||
continue;
|
||||
} else if (next == Token.INTERPOLATION_START) {
|
||||
out.append(AnsiTheme.SYNTAX_STRING_ESCAPE, lexer.text());
|
||||
out.appendSandboxed(() -> doHighlightNormal(out, lexer.next(), lexer, Token.RPAREN));
|
||||
out.append(AnsiTheme.SYNTAX_STRING_ESCAPE, lexer.text());
|
||||
lexer.next();
|
||||
}
|
||||
out.append(lexer.text());
|
||||
next = advance(out, lexer);
|
||||
}
|
||||
out.append(lexer.text());
|
||||
});
|
||||
}
|
||||
|
||||
private static void doHighlightNormal(AnsiStringBuilder out, Token next, Lexer lexer, Token end) {
|
||||
{
|
||||
while (next != end && next != Token.EOF) {
|
||||
if (constant.contains(next)) {
|
||||
out.append(AnsiTheme.SYNTAX_CONSTANT, lexer.text());
|
||||
} else if (operator.contains(next)) {
|
||||
out.append(AnsiTheme.SYNTAX_OPERATOR, lexer.text());
|
||||
} else if (control.contains(next)) {
|
||||
out.append(AnsiTheme.SYNTAX_CONTROL, lexer.text());
|
||||
} else if (keyword.contains(next)) {
|
||||
out.append(AnsiTheme.SYNTAX_KEYWORD, lexer.text());
|
||||
} else if (next.isAffix()) {
|
||||
out.append(AnsiTheme.SYNTAX_COMMENT, lexer.text());
|
||||
} else if (number.contains(next)) {
|
||||
out.append(AnsiTheme.SYNTAX_NUMBER, lexer.text());
|
||||
} else if (next == Token.STRING_MULTI_START || next == Token.STRING_START) {
|
||||
highlightString(out, lexer, next);
|
||||
} else {
|
||||
out.append(lexer.text());
|
||||
}
|
||||
next = advance(out, lexer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Token advance(AnsiStringBuilder out, Lexer lexer) {
|
||||
var prevCursor = lexer.getCursor();
|
||||
var next = lexer.next();
|
||||
// fill in any whitespace (includes semicolons)
|
||||
if (lexer.getStartCursor() > prevCursor) {
|
||||
out.append(lexer.textFor(prevCursor, lexer.getStartCursor() - prevCursor));
|
||||
}
|
||||
return next;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user