Skip to content

Commit 56b8607

Browse files
committed
Add syntax highlighting of Pkl code
1 parent dcf3f24 commit 56b8607

File tree

8 files changed

+270
-22
lines changed

8 files changed

+270
-22
lines changed

pkl-cli/src/main/kotlin/org/pkl/cli/CliRepl.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ internal class CliRepl(private val options: CliEvaluatorOptions) : CliCommand(op
7171
options.base.color?.hasColor() ?: false,
7272
options.base.traceMode ?: TraceMode.COMPACT,
7373
)
74-
Repl(options.base.normalizedWorkingDir, server).run()
74+
Repl(options.base.normalizedWorkingDir, server, options.base.color?.hasColor() ?: false).run()
7575
}
7676
}
7777
}

pkl-cli/src/main/kotlin/org/pkl/cli/repl/Repl.kt

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,35 +18,51 @@ package org.pkl.cli.repl
1818
import java.io.IOException
1919
import java.net.URI
2020
import java.nio.file.Path
21+
import java.util.regex.Pattern
2122
import kotlin.io.path.deleteIfExists
2223
import org.fusesource.jansi.Ansi
2324
import org.jline.reader.EndOfFileException
25+
import org.jline.reader.Highlighter
26+
import org.jline.reader.LineReader
2427
import org.jline.reader.LineReader.Option
2528
import org.jline.reader.LineReaderBuilder
2629
import org.jline.reader.UserInterruptException
2730
import org.jline.reader.impl.completer.AggregateCompleter
2831
import org.jline.reader.impl.history.DefaultHistory
2932
import org.jline.terminal.TerminalBuilder
33+
import org.jline.utils.AttributedString
3034
import org.jline.utils.InfoCmp
3135
import org.pkl.core.repl.ReplRequest
3236
import org.pkl.core.repl.ReplResponse
3337
import org.pkl.core.repl.ReplServer
38+
import org.pkl.core.util.AnsiStringBuilder
39+
import org.pkl.core.util.AnsiStringBuilder.AnsiCode
3440
import org.pkl.core.util.IoUtils
41+
import org.pkl.core.util.SyntaxHighlighter
3542

36-
internal class Repl(workingDir: Path, private val server: ReplServer) {
43+
class PklHighlighter : Highlighter {
44+
override fun highlight(reader: LineReader, buffer: String): AttributedString {
45+
val ansi = AnsiStringBuilder(true).apply { SyntaxHighlighter.writeTo(this, buffer) }.toString()
46+
return AttributedString.fromAnsi(ansi)
47+
}
48+
49+
override fun setErrorPattern(pattern: Pattern) {}
50+
51+
override fun setErrorIndex(idx: Int) {}
52+
}
53+
54+
internal class Repl(workingDir: Path, private val server: ReplServer, private val color: Boolean) {
3755
private val terminal = TerminalBuilder.builder().apply { jansi(true) }.build()
3856
private val history = DefaultHistory()
3957
private val reader =
4058
LineReaderBuilder.builder()
4159
.apply {
4260
history(history)
4361
terminal(terminal)
62+
highlighter(PklHighlighter())
4463
completer(AggregateCompleter(CommandCompleter, FileCompleter(workingDir)))
4564
option(Option.DISABLE_EVENT_EXPANSION, true)
46-
variable(
47-
org.jline.reader.LineReader.HISTORY_FILE,
48-
(IoUtils.getPklHomeDir().resolve("repl-history")),
49-
)
65+
variable(LineReader.HISTORY_FILE, (IoUtils.getPklHomeDir().resolve("repl-history")))
5066
}
5167
.build()
5268

@@ -55,6 +71,12 @@ internal class Repl(workingDir: Path, private val server: ReplServer) {
5571
private var maybeQuit = false
5672
private var nextRequestId = 0
5773

74+
private fun String.faint(): String {
75+
val sb = AnsiStringBuilder(color)
76+
sb.append(AnsiCode.FAINT, this)
77+
return sb.toString()
78+
}
79+
5880
fun run() {
5981
// JLine 2 history file is incompatible with JLine 3
6082
IoUtils.getPklHomeDir().resolve("repl-history.bin").deleteIfExists()
@@ -70,11 +92,11 @@ internal class Repl(workingDir: Path, private val server: ReplServer) {
7092
try {
7193
if (continuation) {
7294
nextRequestId -= 1
73-
reader.readLine(" ".repeat("pkl$nextRequestId> ".length))
95+
reader.readLine(" ".repeat("pkl$nextRequestId> ".length).faint())
7496
} else {
75-
reader.readLine("pkl$nextRequestId> ")
97+
reader.readLine("pkl$nextRequestId> ".faint())
7698
}
77-
} catch (e: UserInterruptException) {
99+
} catch (_: UserInterruptException) {
78100
if (!continuation && reader.buffer.length() == 0) {
79101
if (maybeQuit) quit()
80102
else {
@@ -87,7 +109,7 @@ internal class Repl(workingDir: Path, private val server: ReplServer) {
87109
inputBuffer = ""
88110
continuation = false
89111
continue
90-
} catch (e: EndOfFileException) {
112+
} catch (_: EndOfFileException) {
91113
":quit"
92114
}
93115

@@ -111,10 +133,10 @@ internal class Repl(workingDir: Path, private val server: ReplServer) {
111133
} finally {
112134
try {
113135
history.save()
114-
} catch (ignored: IOException) {}
136+
} catch (_: IOException) {}
115137
try {
116138
terminal.close()
117-
} catch (ignored: IOException) {}
139+
} catch (_: IOException) {}
118140
}
119141
}
120142

@@ -124,10 +146,12 @@ internal class Repl(workingDir: Path, private val server: ReplServer) {
124146
candidates.isEmpty() -> {
125147
println("Unknown command: `${inputBuffer.drop(1)}`")
126148
}
149+
127150
candidates.size > 1 -> {
128151
print("Which of the following did you mean? ")
129152
println(candidates.joinToString(separator = " ") { "`:${it.type}`" })
130153
}
154+
131155
else -> {
132156
doExecuteCommand(candidates.single())
133157
}
@@ -193,16 +217,20 @@ internal class Repl(workingDir: Path, private val server: ReplServer) {
193217
is ReplResponse.EvalSuccess -> {
194218
println(response.result)
195219
}
220+
196221
is ReplResponse.EvalError -> {
197222
println(response.message)
198223
}
224+
199225
is ReplResponse.InternalError -> {
200226
throw response.cause
201227
}
228+
202229
is ReplResponse.IncompleteInput -> {
203230
assert(responses.size == 1)
204231
continuation = true
205232
}
233+
206234
else -> throw IllegalStateException("Unexpected response: $response")
207235
}
208236
}

pkl-core/src/main/java/org/pkl/core/repl/ReplServer.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,12 @@
4545
import org.pkl.core.repl.ReplResponse.InvalidRequest;
4646
import org.pkl.core.resource.ResourceReader;
4747
import org.pkl.core.runtime.*;
48+
import org.pkl.core.util.AnsiStringBuilder;
4849
import org.pkl.core.util.EconomicMaps;
4950
import org.pkl.core.util.IoUtils;
5051
import org.pkl.core.util.MutableReference;
5152
import org.pkl.core.util.Nullable;
53+
import org.pkl.core.util.SyntaxHighlighter;
5254
import org.pkl.parser.Parser;
5355
import org.pkl.parser.ParserError;
5456
import org.pkl.parser.syntax.Class;
@@ -172,7 +174,7 @@ private List<ReplResponse> handleEval(Eval request) {
172174
.collect(Collectors.toList());
173175
}
174176

175-
@SuppressWarnings({"StatementWithEmptyBody", "DataFlowIssue"})
177+
@SuppressWarnings({"StatementWithEmptyBody"})
176178
private List<Object> evaluate(
177179
ReplState replState,
178180
String requestId,
@@ -448,7 +450,10 @@ private VmTyped createReplModule(
448450
}
449451

450452
private String render(Object value) {
451-
return VmValueRenderer.multiLine(Integer.MAX_VALUE).render(value);
453+
var sb = new AnsiStringBuilder(true);
454+
var src = VmValueRenderer.multiLine(Integer.MAX_VALUE).render(value);
455+
SyntaxHighlighter.writeTo(sb, src);
456+
return sb.toString();
452457
}
453458

454459
private static class ReplState {

pkl-core/src/main/java/org/pkl/core/runtime/StackTraceRenderer.java

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@
2222
import org.pkl.core.util.AnsiStringBuilder;
2323
import org.pkl.core.util.AnsiTheme;
2424
import org.pkl.core.util.Nullable;
25+
import org.pkl.core.util.SyntaxHighlighter;
2526

2627
public final class StackTraceRenderer {
2728
private final Function<StackFrame, StackFrame> frameTransformer;
@@ -104,9 +105,11 @@ private void renderSourceLine(StackFrame frame, AnsiStringBuilder out, String le
104105

105106
var prefix = frame.getStartLine() + " | ";
106107
out.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
107-
.append(AnsiTheme.STACK_TRACE_LINE_NUMBER, prefix)
108-
.append(sourceLine)
109-
.append('\n')
108+
.append(AnsiTheme.STACK_TRACE_LINE_NUMBER, prefix);
109+
110+
SyntaxHighlighter.writeTo(out, sourceLine);
111+
112+
out.append('\n')
110113
.append(AnsiTheme.STACK_TRACE_MARGIN, leftMargin)
111114
.append(" ".repeat(prefix.length() + startColumn - 1))
112115
.append(AnsiTheme.STACK_TRACE_CARET, "^".repeat(endColumn - startColumn + 1))

pkl-core/src/main/java/org/pkl/core/util/AnsiStringBuilder.java

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,7 +19,7 @@
1919
import java.util.EnumSet;
2020
import java.util.Set;
2121

22-
@SuppressWarnings("DuplicatedCode")
22+
@SuppressWarnings({"DuplicatedCode", "UnusedReturnValue"})
2323
public final class AnsiStringBuilder {
2424
private final StringBuilder builder = new StringBuilder();
2525
private final boolean usingColor;
@@ -108,6 +108,25 @@ public AnsiStringBuilder append(AnsiCode code, Runnable runnable) {
108108
return this;
109109
}
110110

111+
/** Provides a runnable where anything appended is not affected by the existing context. */
112+
public AnsiStringBuilder appendSandboxed(Runnable runnable) {
113+
if (!usingColor) {
114+
runnable.run();
115+
return this;
116+
}
117+
var myCodes = currentCodes;
118+
var myDeclaredCodes = declaredCodes;
119+
currentCodes = EnumSet.noneOf(AnsiCode.class);
120+
declaredCodes = EnumSet.noneOf(AnsiCode.class);
121+
doReset();
122+
runnable.run();
123+
doReset();
124+
currentCodes = myCodes;
125+
declaredCodes = myDeclaredCodes;
126+
doAppendCodes(currentCodes);
127+
return this;
128+
}
129+
111130
/**
112131
* Append a string whose contents are unknown, and might contain ANSI color codes.
113132
*
@@ -180,6 +199,10 @@ public PrintWriter toPrintWriter() {
180199
return new PrintWriter(new StringBuilderWriter(builder));
181200
}
182201

202+
public void setLength(int length) {
203+
builder.setLength(length);
204+
}
205+
183206
/** Builds the data represented by this builder into a {@link String}. */
184207
public String toString() {
185208
// be a good citizen and unset any ansi escape codes currently set.

pkl-core/src/main/java/org/pkl/core/util/AnsiTheme.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
2+
* Copyright © 2024-2025 Apple Inc. and the Pkl project authors. All rights reserved.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -37,5 +37,15 @@ private AnsiTheme() {}
3737
public static final AnsiCode TEST_NAME = AnsiCode.FAINT;
3838
public static final AnsiCode TEST_FACT_SOURCE = AnsiCode.RED;
3939
public static final AnsiCode TEST_FAILURE_MESSAGE = AnsiCode.RED;
40-
public static final Set<AnsiCode> TEST_EXAMPLE_OUTPUT = EnumSet.of(AnsiCode.RED, AnsiCode.BOLD);
40+
public static final EnumSet<AnsiCode> TEST_EXAMPLE_OUTPUT =
41+
EnumSet.of(AnsiCode.RED, AnsiCode.BOLD);
42+
43+
public static final AnsiCode SYNTAX_KEYWORD = AnsiCode.BLUE;
44+
public static final AnsiCode SYNTAX_NUMBER = AnsiCode.GREEN;
45+
public static final AnsiCode SYNTAX_STRING = AnsiCode.YELLOW;
46+
public static final AnsiCode SYNTAX_STRING_ESCAPE = AnsiCode.BRIGHT_YELLOW;
47+
public static final AnsiCode SYNTAX_COMMENT = AnsiCode.FAINT;
48+
public static final AnsiCode SYNTAX_OPERATOR = AnsiCode.RESET;
49+
public static final AnsiCode SYNTAX_CONTROL = AnsiCode.BLUE;
50+
public static final AnsiCode SYNTAX_CONSTANT = AnsiCode.CYAN;
4151
}

0 commit comments

Comments
 (0)