From 3223083324bd8e20bde4dccf240551a9d21185f7 Mon Sep 17 00:00:00 2001 From: Daniel Chao Date: Fri, 24 Oct 2025 03:23:41 -0700 Subject: [PATCH] Format interpolated expressions as single line (#1247) This forces iterpolated expressions to be single-line, so that newline literals within the bounds of two string delimiters can be seen as verbatime newlines in the resulting string. Edge case: in the case of a line comment, it's not possible to keep this as a single line expression. These are kept as multi-line expressions. Also: * Remove `ForceWrap`, this node is not used. * Rename `StringConstant` -> `StringChars` --- .../main/kotlin/org/pkl/formatter/Builder.kt | 324 ++++++++++++------ .../kotlin/org/pkl/formatter/Generator.kt | 6 - .../org/pkl/formatter/ast/FormatNode.kt | 12 +- .../input/single-line-strings.pkl | 7 + .../input/string-interpolation.pkl | 51 +++ .../output/multi-line-strings.pkl | 4 +- .../output/single-line-strings.pkl | 9 + .../output/string-interpolation.pkl | 52 +++ .../java/org/pkl/parser/GenericParser.java | 6 +- .../pkl/parser/syntax/generic/NodeType.java | 2 +- .../org/pkl/parser/GenericSexpRenderer.kt | 4 +- .../kotlin/org/pkl/parser/SexpRenderer.kt | 4 +- 12 files changed, 355 insertions(+), 126 deletions(-) create mode 100644 pkl-formatter/src/test/files/FormatterSnippetTests/input/single-line-strings.pkl create mode 100644 pkl-formatter/src/test/files/FormatterSnippetTests/input/string-interpolation.pkl create mode 100644 pkl-formatter/src/test/files/FormatterSnippetTests/output/single-line-strings.pkl create mode 100644 pkl-formatter/src/test/files/FormatterSnippetTests/output/string-interpolation.pkl diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt index 9b627412..79386ff3 100644 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt +++ b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Builder.kt @@ -37,6 +37,9 @@ internal class Builder(sourceText: String) { private var id: Int = 0 private val source: CharArray = sourceText.toCharArray() private var prevNode: Node? = null + private var noNewlines = false + + private class CannotAvoidNewline : RuntimeException() fun format(node: Node): FormatNode { prevNode = node @@ -49,9 +52,8 @@ internal class Builder(sourceText: String) { NodeType.TERMINAL, NodeType.MODIFIER, NodeType.IDENTIFIER, - NodeType.STRING_CONSTANT, + NodeType.STRING_CHARS, NodeType.STRING_ESCAPE, - NodeType.SINGLE_LINE_STRING_LITERAL_EXPR, NodeType.INT_LITERAL_EXPR, NodeType.FLOAT_LITERAL_EXPR, NodeType.BOOL_LITERAL_EXPR, @@ -64,9 +66,10 @@ internal class Builder(sourceText: String) { NodeType.NOTHING_TYPE, NodeType.SHEBANG, NodeType.OPERATOR -> Text(node.text(source)) - NodeType.STRING_NEWLINE -> ForceLine + NodeType.STRING_NEWLINE -> mustForceLine() NodeType.MODULE_DECLARATION -> formatModuleDeclaration(node) NodeType.MODULE_DEFINITION -> formatModuleDefinition(node) + NodeType.SINGLE_LINE_STRING_LITERAL_EXPR -> formatSingleLineString(node) NodeType.MULTI_LINE_STRING_LITERAL_EXPR -> formatMultilineString(node) NodeType.ANNOTATION -> formatAnnotation(node) NodeType.TYPEALIAS -> formatTypealias(node) @@ -77,13 +80,13 @@ internal class Builder(sourceText: String) { NodeType.PARAMETER_LIST_ELEMENTS -> formatParameterListElements(node) NodeType.TYPE_PARAMETER_LIST -> formatTypeParameterList(node) NodeType.TYPE_PARAMETER_LIST_ELEMENTS -> formatParameterListElements(node) - NodeType.TYPE_PARAMETER -> Group(newId(), formatGeneric(node.children, SpaceOrLine)) + NodeType.TYPE_PARAMETER -> Group(newId(), formatGeneric(node.children, spaceOrLine())) NodeType.PARAMETER -> formatParameter(node) NodeType.EXTENDS_CLAUSE, NodeType.AMENDS_CLAUSE -> formatAmendsExtendsClause(node) NodeType.IMPORT_LIST -> formatImportList(node) NodeType.IMPORT -> formatImport(node) - NodeType.IMPORT_ALIAS -> Group(newId(), formatGeneric(node.children, SpaceOrLine)) + NodeType.IMPORT_ALIAS -> Group(newId(), formatGeneric(node.children, spaceOrLine())) NodeType.CLASS -> formatClass(node) NodeType.CLASS_HEADER -> formatClassHeader(node) NodeType.CLASS_HEADER_EXTENDS -> formatClassHeaderExtends(node) @@ -167,7 +170,7 @@ internal class Builder(sourceText: String) { private fun formatModule(node: Node): FormatNode { val nodes = formatGeneric(node.children) { prev, next -> - if (prev.linesBetween(next) > 1) TWO_NEWLINES else ForceLine + if (prev.linesBetween(next) > 1) TWO_NEWLINES else forceLine() } return Nodes(nodes) } @@ -179,7 +182,7 @@ internal class Builder(sourceText: String) { private fun formatModuleDefinition(node: Node): FormatNode { val (prefixes, nodes) = splitPrefixes(node.children) val fnodes = - formatGenericWithGen(nodes, SpaceOrLine) { node, next -> + formatGenericWithGen(nodes, spaceOrLine()) { node, next -> if (next == null) { indent(format(node)) } else { @@ -191,7 +194,7 @@ internal class Builder(sourceText: String) { res } else { val sep = getSeparator(prefixes.last(), nodes.first()) - Nodes(formatGeneric(prefixes, SpaceOrLine) + listOf(sep, res)) + Nodes(formatGeneric(prefixes, spaceOrLine()) + listOf(sep, res)) } } @@ -217,10 +220,10 @@ internal class Builder(sourceText: String) { // short circuit if (node.children.size == 1) return format(node.children[0]) - val first = listOf(format(node.children[0]), Line) + val first = listOf(format(node.children[0]), line()) val nodes = formatGeneric(node.children.drop(1)) { n1, _ -> - if (n1.type == NodeType.TERMINAL) null else Line + if (n1.type == NodeType.TERMINAL) null else line() } return Group(newId(), first + listOf(Indent(nodes))) } @@ -240,28 +243,28 @@ internal class Builder(sourceText: String) { } private fun formatAmendsExtendsClause(node: Node): FormatNode { - val prefix = formatGeneric(node.children.dropLast(1), SpaceOrLine) + val prefix = formatGeneric(node.children.dropLast(1), spaceOrLine()) // string constant val suffix = Indent(listOf(format(node.children.last()))) - return Group(newId(), prefix + listOf(SpaceOrLine) + suffix) + return Group(newId(), prefix + listOf(spaceOrLine()) + suffix) } private fun formatImport(node: Node): FormatNode { return Group( newId(), - formatGenericWithGen(node.children, SpaceOrLine) { node, _ -> + formatGenericWithGen(node.children, spaceOrLine()) { node, _ -> if (node.isTerminal("import")) format(node) else indent(format(node)) }, ) } private fun formatAnnotation(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, SpaceOrLine)) + return Group(newId(), formatGeneric(node.children, spaceOrLine())) } private fun formatTypealias(node: Node): FormatNode { val nodes = - groupNonPrefixes(node) { children -> Group(newId(), formatGeneric(children, SpaceOrLine)) } + groupNonPrefixes(node) { children -> Group(newId(), formatGeneric(children, spaceOrLine())) } return Nodes(nodes) } @@ -270,19 +273,19 @@ internal class Builder(sourceText: String) { } private fun formatTypealiasBody(node: Node): FormatNode { - return Indent(formatGeneric(node.children, SpaceOrLine)) + return Indent(formatGeneric(node.children, spaceOrLine())) } private fun formatClass(node: Node): FormatNode { - return Nodes(formatGeneric(node.children, SpaceOrLine)) + return Nodes(formatGeneric(node.children, spaceOrLine())) } private fun formatClassHeader(node: Node): FormatNode { - return groupOnSpace(formatGeneric(node.children, SpaceOrLine)) + return groupOnSpace(formatGeneric(node.children, spaceOrLine())) } private fun formatClassHeaderExtends(node: Node): FormatNode { - return indent(Group(newId(), formatGeneric(node.children, SpaceOrLine))) + return indent(Group(newId(), formatGeneric(node.children, spaceOrLine()))) } private fun formatClassBody(node: Node): FormatNode { @@ -291,14 +294,14 @@ internal class Builder(sourceText: String) { // no members return Nodes(formatGeneric(children, null)) } - return Group(newId(), formatGeneric(children, ForceLine)) + return Group(newId(), formatGeneric(children, forceLine())) } private fun formatClassBodyElements(node: Node): FormatNode { val nodes = formatGeneric(node.children) { prev, next -> val lineDiff = prev.linesBetween(next) - if (lineDiff > 1 || lineDiff == 0) TWO_NEWLINES else ForceLine + if (lineDiff > 1 || lineDiff == 0) TWO_NEWLINES else forceLine() } return Indent(nodes) } @@ -313,8 +316,9 @@ internal class Builder(sourceText: String) { val nodes = groupNonPrefixes(node) { children -> val nodes = - formatGenericWithGen(children, { _, _ -> if (sameLine) Space else SpaceOrLine }) { node, _ - -> + formatGenericWithGen(children, { _, _ -> if (sameLine) Space else spaceOrLine() }) { + node, + _ -> if ((node.isExpressionOrPropertyBody()) && !sameLine) { indent(format(node)) } else format(node) @@ -330,11 +334,11 @@ internal class Builder(sourceText: String) { type == NodeType.OBJECT_PROPERTY_BODY private fun formatClassPropertyHeader(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, SpaceOrLine)) + return Group(newId(), formatGeneric(node.children, spaceOrLine())) } private fun formatClassPropertyHeaderBegin(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, SpaceOrLine)) + return Group(newId(), formatGeneric(node.children, spaceOrLine())) } private fun formatClassPropertyBody(node: Node): FormatNode { @@ -349,7 +353,7 @@ internal class Builder(sourceText: String) { val idx = node.children.indexOfFirst { it.type == NodeType.CLASS_METHOD_HEADER } val prefixNodes = node.children.subList(0, idx) prefixes += formatGeneric(prefixNodes, null) - prefixes += getSeparator(prefixNodes.last(), node.children[idx], ForceLine) + prefixes += getSeparator(prefixNodes.last(), node.children[idx], forceLine()) node.children.subList(idx, node.children.size) } @@ -359,7 +363,7 @@ internal class Builder(sourceText: String) { val headerGroupId = newId() val methodGroupId = newId() val headerNodes = - formatGenericWithGen(header, SpaceOrLine) { node, _ -> + formatGenericWithGen(header, spaceOrLine()) { node, _ -> if (node.type == NodeType.PARAMETER_LIST) { formatParameterList(node, id = headerGroupId) } else { @@ -385,7 +389,7 @@ internal class Builder(sourceText: String) { if (isSameLineBody) { formatGeneric(bodyNodes, Space) } else { - formatGenericWithGen(bodyNodes, SpaceOrLine) { node, next -> + formatGenericWithGen(bodyNodes, spaceOrLine()) { node, next -> if (next == null) indent(format(node)) else format(node) } } @@ -409,7 +413,7 @@ internal class Builder(sourceText: String) { private fun formatParameter(node: Node): FormatNode { if (node.children.size == 1) return format(node.children[0]) // underscore - return Group(newId(), formatGeneric(node.children, SpaceOrLine)) + return Group(newId(), formatGeneric(node.children, spaceOrLine())) } private fun formatParameterList(node: Node, id: Int? = null): FormatNode { @@ -420,9 +424,9 @@ internal class Builder(sourceText: String) { if (prev.isTerminal("(") || next.isTerminal(")")) { if (next.isTerminal(")")) { // trailing comma - IfWrap(groupId, nodes(Text(","), Line), Line) - } else Line - } else SpaceOrLine + ifWrap(groupId, nodes(Text(","), line()), line()) + } else line() + } else spaceOrLine() } return if (id != null) Nodes(nodes) else Group(groupId, nodes) } @@ -436,12 +440,12 @@ internal class Builder(sourceText: String) { node.children, { prev, next -> if (prev.isTerminal("(") || next.isTerminal(")")) { - val node = if (hasTrailingLambda) Empty else Line + val node = if (hasTrailingLambda) Empty else line() if (next.isTerminal(")") && !hasTrailingLambda) { // trailing comma - IfWrap(groupId, nodes(Text(","), node), node) + ifWrap(groupId, nodes(Text(","), node), node) } else node - } else SpaceOrLine + } else spaceOrLine() }, ) { node, _ -> if (node.type == NodeType.ARGUMENT_LIST_ELEMENTS) { @@ -460,9 +464,9 @@ internal class Builder(sourceText: String) { return if (twoBy2) { val pairs = pairArguments(children) val nodes = - formatGenericWithGen(pairs, SpaceOrLine) { node, _ -> + formatGenericWithGen(pairs, spaceOrLine()) { node, _ -> if (node.type == NodeType.ARGUMENT_LIST_ELEMENTS) { - Group(newId(), formatGeneric(node.children, SpaceOrLine)) + Group(newId(), formatGeneric(node.children, spaceOrLine())) } else { format(node) } @@ -473,17 +477,17 @@ internal class Builder(sourceText: String) { val splitIndex = children.indexOfLast { it.type in SAME_LINE_EXPRS } val normalParams = children.subList(0, splitIndex) val lastParam = children.subList(splitIndex, children.size) - val trailingNode = if (endsWithClosingBracket(children[splitIndex])) Empty else Line - val lastNodes = formatGeneric(lastParam, SpaceOrLine) + val trailingNode = if (endsWithClosingBracket(children[splitIndex])) Empty else line() + val lastNodes = formatGeneric(lastParam, spaceOrLine()) if (normalParams.isEmpty()) { nodes(Group(newId(), lastNodes), trailingNode) } else { val separator = getSeparator(normalParams.last(), lastParam[0], Space) - val paramNodes = formatGeneric(normalParams, SpaceOrLine) + val paramNodes = formatGeneric(normalParams, spaceOrLine()) nodes(Group(newId(), paramNodes), separator, Group(newId(), lastNodes), trailingNode) } } else { - Indent(formatGeneric(children, SpaceOrLine)) + Indent(formatGeneric(children, spaceOrLine())) } } @@ -531,7 +535,7 @@ internal class Builder(sourceText: String) { } private fun formatParameterListElements(node: Node): FormatNode { - return Indent(formatGeneric(node.children, SpaceOrLine)) + return Indent(formatGeneric(node.children, spaceOrLine())) } private fun formatTypeParameterList(node: Node): FormatNode { @@ -542,9 +546,9 @@ internal class Builder(sourceText: String) { if (prev.isTerminal("<") || next.isTerminal(">")) { if (next.isTerminal(">")) { // trailing comma - IfWrap(id, nodes(Text(","), Line), Line) - } else Line - } else SpaceOrLine + ifWrap(id, nodes(Text(","), line()), line()) + } else line() + } else spaceOrLine() } return Group(id, nodes) } @@ -552,10 +556,10 @@ internal class Builder(sourceText: String) { private fun formatObjectParameterList(node: Node): FormatNode { // object param lists don't have trailing commas, as they have a trailing -> val groupId = newId() - val nonWrappingNodes = Nodes(formatGeneric(node.children, SpaceOrLine)) + val nonWrappingNodes = Nodes(formatGeneric(node.children, spaceOrLine())) // double indent the params if they wrap - val wrappingNodes = indent(Indent(listOf(Line) + nonWrappingNodes)) - return Group(groupId, listOf(IfWrap(groupId, wrappingNodes, nodes(Space, nonWrappingNodes)))) + val wrappingNodes = indent(Indent(listOf(line()) + nonWrappingNodes)) + return Group(groupId, listOf(ifWrap(groupId, wrappingNodes, nodes(Space, nonWrappingNodes)))) } private fun formatObjectBody(node: Node): FormatNode { @@ -568,8 +572,8 @@ internal class Builder(sourceText: String) { if (next.type == NodeType.OBJECT_PARAMETER_LIST) Empty else if (prev.isTerminal("{") || next.isTerminal("}")) { val lines = prev.linesBetween(next) - if (lines == 0) SpaceOrLine else ForceLine - } else SpaceOrLine + if (lines == 0) spaceOrLine() else forceSpaceyLine() + } else spaceOrLine() }, ) { node, _ -> if (node.type == NodeType.OBJECT_MEMBER_LIST) { @@ -584,8 +588,8 @@ internal class Builder(sourceText: String) { formatGeneric(node.children) { prev, next -> val lines = prev.linesBetween(next) when (lines) { - 0 -> IfWrap(groupId, Line, Text("; ")) - 1 -> ForceLine + 0 -> ifWrap(groupId, line(), Text("; ")) + 1 -> forceLine() else -> TWO_NEWLINES } } @@ -593,7 +597,7 @@ internal class Builder(sourceText: String) { } private fun formatObjectEntryHeader(node: Node): FormatNode { - return Group(newId(), formatGeneric(node.children, SpaceOrLine)) + return Group(newId(), formatGeneric(node.children, spaceOrLine())) } private fun formatForGenerator(node: Node): FormatNode { @@ -603,7 +607,7 @@ internal class Builder(sourceText: String) { prev.type == NodeType.FOR_GENERATOR_HEADER || next.type == NodeType.FOR_GENERATOR_HEADER ) { Space - } else SpaceOrLine + } else spaceOrLine() } return Group(newId(), nodes) } @@ -611,7 +615,7 @@ internal class Builder(sourceText: String) { private fun formatForGeneratorHeader(node: Node): FormatNode { val nodes = formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) Line else null + if (prev.isTerminal("(") || next.isTerminal(")")) line() else null } return Group(newId(), nodes) } @@ -620,7 +624,7 @@ internal class Builder(sourceText: String) { val nodes = formatGenericWithGen( node.children, - { _, next -> if (next.type in SAME_LINE_EXPRS) Space else SpaceOrLine }, + { _, next -> if (next.type in SAME_LINE_EXPRS) Space else spaceOrLine() }, ) { node, _ -> if (node.type.isExpression && node.type !in SAME_LINE_EXPRS) indent(format(node)) else format(node) @@ -629,7 +633,7 @@ internal class Builder(sourceText: String) { } private fun formatForGeneratorHeaderDefinitionHeader(node: Node): FormatNode { - val nodes = formatGeneric(node.children, SpaceOrLine) + val nodes = formatGeneric(node.children, spaceOrLine()) return Group(newId(), nodes) } @@ -643,7 +647,7 @@ internal class Builder(sourceText: String) { ) { Space } else { - SpaceOrLine + spaceOrLine() } } return Group(newId(), nodes) @@ -653,7 +657,9 @@ internal class Builder(sourceText: String) { val nodes = formatGenericWithGen( node.children, - { prev, next -> if (prev.isTerminal("(") || next.isTerminal(")")) Line else SpaceOrLine }, + { prev, next -> + if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() + }, ) { node, _ -> if (!node.type.isAffix && node.type != NodeType.TERMINAL) { indent(format(node)) @@ -664,7 +670,7 @@ internal class Builder(sourceText: String) { private fun formatMemberPredicate(node: Node): FormatNode { val nodes = - formatGenericWithGen(node.children, SpaceOrLine) { node, next -> + formatGenericWithGen(node.children, spaceOrLine()) { node, next -> if (next == null && node.type != NodeType.OBJECT_BODY) { indent(format(node)) } else format(node) @@ -672,8 +678,46 @@ internal class Builder(sourceText: String) { return Group(newId(), nodes) } + private fun formatStringParts(nodes: List): List { + return buildList { + var isInStringInterpolation = false + val cursor = nodes.iterator().peekable() + var prev: Node? = null + while (cursor.hasNext()) { + if (isInStringInterpolation) { + val prevNoNewlines = noNewlines + noNewlines = true + val elems = cursor.takeUntilBefore { it.isTerminal(")") } + getSeparator(prev!!, elems.first(), { _, _ -> null })?.let { add(it) } + val formatted = + try { + formatGeneric(elems, null) + } catch (_: CannotAvoidNewline) { + noNewlines = false + formatGeneric(elems, null) + } + addAll(formatted) + getSeparator(elems.last(), cursor.peek(), { _, _ -> null })?.let { add(it) } + noNewlines = prevNoNewlines + isInStringInterpolation = false + continue + } + val elem = cursor.next() + if (elem.type == NodeType.TERMINAL && elem.text().endsWith("(")) { + isInStringInterpolation = true + } + add(format(elem)) + prev = elem + } + } + } + + private fun formatSingleLineString(node: Node): FormatNode { + return Group(newId(), formatStringParts(node.children)) + } + private fun formatMultilineString(node: Node): FormatNode { - val nodes = formatGeneric(node.children, null) + val nodes = formatStringParts(node.children) return MultilineStringGroup(node.children.last().span.colBegin, nodes) } @@ -682,7 +726,7 @@ internal class Builder(sourceText: String) { formatGeneric(node.children) { _, next -> if (next.type == NodeType.IF_ELSE_EXPR && next.children[0].type == NodeType.IF_EXPR) { Space - } else SpaceOrLine + } else spaceOrLine() } return Group(newId(), nodes) } @@ -690,7 +734,7 @@ internal class Builder(sourceText: String) { private fun formatIfHeader(node: Node): FormatNode { val nodes = formatGeneric(node.children) { _, next -> - if (next.type == NodeType.IF_CONDITION) Space else SpaceOrLine + if (next.type == NodeType.IF_CONDITION) Space else spaceOrLine() } return Group(newId(), nodes) } @@ -698,7 +742,7 @@ internal class Builder(sourceText: String) { private fun formatIfCondition(node: Node): FormatNode { val nodes = formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) Line else SpaceOrLine + if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() } return Group(newId(), nodes) } @@ -723,12 +767,12 @@ internal class Builder(sourceText: String) { } private fun formatNewExpr(node: Node): FormatNode { - val nodes = formatGeneric(node.children, SpaceOrLine) + val nodes = formatGeneric(node.children, spaceOrLine()) return Group(newId(), nodes) } private fun formatNewHeader(node: Node): FormatNode { - val nodes = formatGeneric(node.children, SpaceOrLine) + val nodes = formatGeneric(node.children, spaceOrLine()) return Group(newId(), nodes) } @@ -737,7 +781,9 @@ internal class Builder(sourceText: String) { val nodes = formatGenericWithGen( node.children, - { prev, next -> if (prev.isTerminal("(") || next.isTerminal(")")) Line else SpaceOrLine }, + { prev, next -> + if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() + }, ) { node, _ -> if (node.type.isExpression) indent(format(node)) else format(node) } @@ -757,7 +803,7 @@ internal class Builder(sourceText: String) { val expr = body.children.find { it.type.isExpression }!! isSameLineExpr(expr) } - val sep = if (sameLine) Space else SpaceOrLine + val sep = if (sameLine) Space else spaceOrLine() val bodySep = getSeparator(params.last(), rest.first(), sep) val nodes = formatGeneric(params, sep) @@ -775,7 +821,7 @@ internal class Builder(sourceText: String) { val nodes = formatGenericWithGen( node.children, - { _, next -> if (next.type == NodeType.LET_PARAMETER_DEFINITION) Space else SpaceOrLine }, + { _, next -> if (next.type == NodeType.LET_PARAMETER_DEFINITION) Space else spaceOrLine() }, ) { node, next -> if (next == null) { if (node.type == NodeType.LET_EXPR) { @@ -791,7 +837,7 @@ internal class Builder(sourceText: String) { private fun formatLetParameterDefinition(node: Node): FormatNode { val nodes = formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) Line else SpaceOrLine + if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() } return Group(newId(), nodes) } @@ -810,17 +856,17 @@ internal class Builder(sourceText: String) { when (prev.text()) { ".", "?." -> null - "-" -> SpaceOrLine + "-" -> spaceOrLine() else -> Space } } else if (next.type == NodeType.OPERATOR) { when (next.text()) { ".", - "?." -> if (hasMultipleLambdas) ForceLine else Line + "?." -> if (hasMultipleLambdas) forceLine() else line() "-" -> Space - else -> SpaceOrLine + else -> spaceOrLine() } - } else SpaceOrLine + } else spaceOrLine() } val shouldGroup = node.children.size == flat.size return Group(newId(), indentAfterFirstNewline(nodes, shouldGroup)) @@ -843,7 +889,7 @@ internal class Builder(sourceText: String) { val nodes = formatGenericWithGen( node.children, - { prev, next -> if (prev.isTerminal("(") || next.isTerminal(")")) Line else null }, + { prev, next -> if (prev.isTerminal("(") || next.isTerminal(")")) line() else null }, ) { node, _ -> if (node.type.isExpression) indent(format(node)) else format(node) } @@ -851,13 +897,13 @@ internal class Builder(sourceText: String) { } private fun formatDeclaredType(node: Node): FormatNode { - return Nodes(formatGeneric(node.children, SpaceOrLine)) + return Nodes(formatGeneric(node.children, spaceOrLine())) } private fun formatConstrainedType(node: Node): FormatNode { val nodes = formatGeneric(node.children) { _, next -> - if (next.type == NodeType.CONSTRAINED_TYPE_CONSTRAINT) null else SpaceOrLine + if (next.type == NodeType.CONSTRAINED_TYPE_CONSTRAINT) null else spaceOrLine() } return Group(newId(), nodes) } @@ -866,7 +912,7 @@ internal class Builder(sourceText: String) { val nodes = formatGeneric(node.children) { prev, next -> when { - next.isTerminal("|") -> SpaceOrLine + next.isTerminal("|") -> spaceOrLine() prev.isTerminal("|") -> Space else -> null } @@ -878,7 +924,9 @@ internal class Builder(sourceText: String) { val nodes = formatGenericWithGen( node.children, - { prev, next -> if (prev.isTerminal("(") || next.isTerminal(")")) Line else SpaceOrLine }, + { prev, next -> + if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() + }, ) { node, next -> if (next == null) indent(format(node)) else format(node) } @@ -890,13 +938,13 @@ internal class Builder(sourceText: String) { val groupId = newId() val nodes = formatGeneric(node.children) { prev, next -> - if (prev.isTerminal("(") || next.isTerminal(")")) Line else SpaceOrLine + if (prev.isTerminal("(") || next.isTerminal(")")) line() else spaceOrLine() } return Group(groupId, nodes) } private fun formatParenthesizedTypeElements(node: Node): FormatNode { - return indent(Group(newId(), formatGeneric(node.children, SpaceOrLine))) + return indent(Group(newId(), formatGeneric(node.children, spaceOrLine()))) } private fun formatTypeAnnotation(node: Node): FormatNode { @@ -907,7 +955,7 @@ internal class Builder(sourceText: String) { val nodes = mutableListOf() val children = node.children.groupBy { it.type.isAffix } if (children[true] != null) { - nodes += formatGeneric(children[true]!!, SpaceOrLine) + nodes += formatGeneric(children[true]!!, spaceOrLine()) } val modifiers = children[false]!!.sortedBy(::modifierPrecedence) nodes += formatGeneric(modifiers, Space) @@ -918,8 +966,8 @@ internal class Builder(sourceText: String) { val nodes = mutableListOf() val children = node.children.groupBy { it.type.isAffix } if (children[true] != null) { - nodes += formatGeneric(children[true]!!, SpaceOrLine) - nodes += ForceLine + nodes += formatGeneric(children[true]!!, spaceOrLine()) + nodes += forceLine() } val allImports = children[false]!! @@ -952,27 +1000,27 @@ internal class Builder(sourceText: String) { if (absolute != null) { for ((i, imp) in absolute.withIndex()) { - if (i > 0) nodes += ForceLine + if (i > 0) nodes += forceLine() nodes += format(imp) } - if (projects != null || relatives != null) nodes += ForceLine + if (projects != null || relatives != null) nodes += forceLine() shouldNewline = true } if (projects != null) { - if (shouldNewline) nodes += ForceLine + if (shouldNewline) nodes += forceLine() for ((i, imp) in projects.withIndex()) { - if (i > 0) nodes += ForceLine + if (i > 0) nodes += forceLine() nodes += format(imp) } - if (relatives != null) nodes += ForceLine + if (relatives != null) nodes += forceLine() shouldNewline = true } if (relatives != null) { - if (shouldNewline) nodes += ForceLine + if (shouldNewline) nodes += forceLine() for ((i, imp) in relatives.withIndex()) { - if (i > 0) nodes += ForceLine + if (i > 0) nodes += forceLine() nodes += format(imp) } } @@ -1005,7 +1053,7 @@ internal class Builder(sourceText: String) { // skip semicolons val children = children.filter { !it.isSemicolon() } // short circuit - if (children.isEmpty()) return listOf(SpaceOrLine) + if (children.isEmpty()) return listOf(spaceOrLine()) if (children.size == 1) return listOf(format(children[0])) val nodes = mutableListOf() @@ -1046,19 +1094,19 @@ internal class Builder(sourceText: String) { val prefixes = children.subList(0, index) val nodes = children.subList(index, children.size) val res = mutableListOf() - res += formatGeneric(prefixes, SpaceOrLine) + res += formatGeneric(prefixes, spaceOrLine()) res += getSeparator(prefixes.last(), nodes.first()) res += groupFn(nodes) return res } private fun getImportUrl(node: Node): String = - node.findChildByType(NodeType.STRING_CONSTANT)!!.text().drop(1).dropLast(1) + node.findChildByType(NodeType.STRING_CHARS)!!.text().drop(1).dropLast(1) private fun getSeparator( prev: Node, next: Node, - separator: FormatNode = SpaceOrLine, + separator: FormatNode = spaceOrLine(), ): FormatNode { return getSeparator(prev, next) { _, _ -> separator }!! } @@ -1073,19 +1121,21 @@ internal class Builder(sourceText: String) { if (prev.linesBetween(next) > 1) { TWO_NEWLINES } else { - ForceLine + mustForceLine() } } hasTrailingAffix(prev, next) -> Space - prev.type == NodeType.DOC_COMMENT || prev.type == NodeType.ANNOTATION -> ForceLine + prev.type == NodeType.DOC_COMMENT -> mustForceLine() + prev.type == NodeType.ANNOTATION -> forceLine() prev.type in FORCE_LINE_AFFIXES || next.type.isAffix -> { if (prev.linesBetween(next) > 1) { TWO_NEWLINES } else { - ForceLine + mustForceLine() } } - prev.type == NodeType.BLOCK_COMMENT -> if (prev.linesBetween(next) > 0) ForceLine else Space + prev.type == NodeType.BLOCK_COMMENT -> + if (prev.linesBetween(next) > 0) forceSpaceyLine() else Space next.type in EMPTY_SUFFIXES || prev.isTerminal("[", "!", "@", "[[") || next.isTerminal("]", "?", ",") -> null @@ -1098,6 +1148,30 @@ internal class Builder(sourceText: String) { } } + private fun line(): FormatNode { + return if (noNewlines) Empty else Line + } + + private fun spaceOrLine(): FormatNode { + return if (noNewlines) Space else SpaceOrLine + } + + private fun mustForceLine(): FormatNode { + return if (noNewlines) throw CannotAvoidNewline() else ForceLine + } + + private fun forceLine(): FormatNode { + return if (noNewlines) Empty else ForceLine + } + + private fun forceSpaceyLine(): FormatNode { + return if (noNewlines) Space else ForceLine + } + + private fun ifWrap(id: Int, ifWrap: FormatNode, ifNotWrap: FormatNode): FormatNode { + return if (noNewlines) ifNotWrap else IfWrap(id, ifWrap, ifNotWrap) + } + private fun hasTrailingAffix(node: Node, next: Node): Boolean { if (node.span.lineEnd < next.span.lineBegin) return false var n: Node? = next @@ -1201,8 +1275,8 @@ internal class Builder(sourceText: String) { private class ImportComparator(private val source: CharArray) : Comparator { override fun compare(o1: Node, o2: Node): Int { - val import1 = o1.findChildByType(NodeType.STRING_CONSTANT)?.text(source) - val import2 = o2.findChildByType(NodeType.STRING_CONSTANT)?.text(source) + val import1 = o1.findChildByType(NodeType.STRING_CHARS)?.text(source) + val import2 = o2.findChildByType(NodeType.STRING_CHARS)?.text(source) if (import1 == null || import2 == null) { // should never happen throw RuntimeException("ImportComparator: not an import") @@ -1222,7 +1296,7 @@ internal class Builder(sourceText: String) { // returns true if this node is not an affix or terminal private fun Node.isProper(): Boolean = !type.isAffix && type != NodeType.TERMINAL - private fun List.splitOn(pred: (T) -> Boolean): Pair, List> { + private inline fun List.splitOn(pred: (T) -> Boolean): Pair, List> { val index = indexOfFirst { pred(it) } return if (index == -1) { Pair(this, emptyList()) @@ -1243,6 +1317,52 @@ internal class Builder(sourceText: String) { return false } + class PeekableIterator(private val iterator: Iterator) : Iterator { + private var peek: T? = null + + private var hasPeek = false + + override fun next(): T { + return if (hasPeek) { + hasPeek = false + peek!! + } else { + iterator.next() + } + } + + override fun hasNext(): Boolean { + return if (hasPeek) true else iterator.hasNext() + } + + fun peek(): T { + if (!hasNext()) { + throw NoSuchElementException() + } + if (hasPeek) { + return peek!! + } + peek = iterator.next() + hasPeek = true + return peek!! + } + + inline fun takeUntilBefore(predicate: (T) -> Boolean): List { + return buildList { + while (true) { + if (!hasNext() || predicate(peek())) { + return@buildList + } + add(next()) + } + } + } + } + + private fun Iterator.peekable(): PeekableIterator { + return PeekableIterator(this) + } + companion object { private val ABSOLUTE_URL_REGEX = Regex("""\w+:.*""") diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt index e5c9f8f2..947d374a 100644 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt +++ b/pkl-formatter/src/main/kotlin/org/pkl/formatter/Generator.kt @@ -17,7 +17,6 @@ package org.pkl.formatter import org.pkl.formatter.ast.Empty import org.pkl.formatter.ast.ForceLine -import org.pkl.formatter.ast.ForceWrap import org.pkl.formatter.ast.FormatNode import org.pkl.formatter.ast.Group import org.pkl.formatter.ast.IfWrap @@ -56,11 +55,6 @@ internal class Generator { } node.nodes.forEach { node(it, wrap) } } - is ForceWrap -> { - wrapped += node.id - val wrap = Wrap.ENABLED - node.nodes.forEach { node(it, wrap) } - } is IfWrap -> { if (wrapped.contains(node.id)) { node(node.ifWrap, Wrap.ENABLED) diff --git a/pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt b/pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt index 37361a4f..6412c55d 100644 --- a/pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt +++ b/pkl-formatter/src/main/kotlin/org/pkl/formatter/ast/FormatNode.kt @@ -30,14 +30,14 @@ sealed interface FormatNode { is Nodes -> nodes.sumOf { it.width(wrapped) } is Group -> nodes.sumOf { it.width(wrapped) } is Indent -> nodes.sumOf { it.width(wrapped) } - is ForceWrap -> nodes.sumOf { it.width(wrapped + id) } is IfWrap -> if (id in wrapped) ifWrap.width(wrapped) else ifNotWrap.width(wrapped) is Text -> text.length - is SpaceOrLine, - is Space -> 1 - is ForceLine, + SpaceOrLine, + Space -> 1 + ForceLine, is MultilineStringGroup -> Generator.MAX - else -> 0 + Empty -> 0 + Line -> 0 } } @@ -59,8 +59,6 @@ data class Nodes(val nodes: List) : FormatNode data class Group(val id: Int, val nodes: List) : FormatNode -data class ForceWrap(val id: Int, val nodes: List) : FormatNode - data class MultilineStringGroup(val endQuoteCol: Int, val nodes: List) : FormatNode data class IfWrap(val id: Int, val ifWrap: FormatNode, val ifNotWrap: FormatNode) : FormatNode diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/input/single-line-strings.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/input/single-line-strings.pkl new file mode 100644 index 00000000..fc6769a1 --- /dev/null +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/input/single-line-strings.pkl @@ -0,0 +1,7 @@ +foo1 = "some string" + +foo2 = "some string with \( new { x = 1; y = 2 }) interpolation" + +foo3 = "some reeeeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaally long string with \( new { x = 1; y = 2 } ) interpolation" + +foo4 = "some reeeeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaally long string with \( foo.bar.baz() ) qualified access" diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/input/string-interpolation.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/input/string-interpolation.pkl new file mode 100644 index 00000000..ad2a36c9 --- /dev/null +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/input/string-interpolation.pkl @@ -0,0 +1,51 @@ +prop1 = + """ + asd \(new { bar = 1 }) asd + """ + +prop2 = + """ + \(let (bar = 15) bar + new { qux = 15 }.toString()) + """ + +prop3 = + """ + \(new { + // some comment + foo = 1 + + // some comment + bar = 2 + }) + """ + +prop4 = + """ + \(1 + /* block comment */ 2) + """ + +prop5 = + """ + \(""" + foo + bar + baz + """) + """ + +prop6 = "\(// some line comment + /* some block comment */ + "\(""" + one + two + three + """)" + // some line comment again +)" + +prop7 = "\( +5 +// trailing line comment +)" + +prop8 = "\(new { foo = 1 bar = 2 baz = 3 })" diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/output/multi-line-strings.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/output/multi-line-strings.pkl index b4bc3b3c..144b585d 100644 --- a/pkl-formatter/src/test/files/FormatterSnippetTests/output/multi-line-strings.pkl +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/output/multi-line-strings.pkl @@ -1,8 +1,6 @@ foo = """ - asd \(new { - bar = 1 - }) asd + asd \(new { bar = 1 }) asd """ bar = diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/output/single-line-strings.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/output/single-line-strings.pkl new file mode 100644 index 00000000..48f3424b --- /dev/null +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/output/single-line-strings.pkl @@ -0,0 +1,9 @@ +foo1 = "some string" + +foo2 = "some string with \(new { x = 1; y = 2 }) interpolation" + +foo3 = + "some reeeeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaally long string with \(new { x = 1; y = 2 }) interpolation" + +foo4 = + "some reeeeaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaally long string with \(foo.bar.baz()) qualified access" diff --git a/pkl-formatter/src/test/files/FormatterSnippetTests/output/string-interpolation.pkl b/pkl-formatter/src/test/files/FormatterSnippetTests/output/string-interpolation.pkl new file mode 100644 index 00000000..b78d7eab --- /dev/null +++ b/pkl-formatter/src/test/files/FormatterSnippetTests/output/string-interpolation.pkl @@ -0,0 +1,52 @@ +prop1 = + """ + asd \(new { bar = 1 }) asd + """ + +prop2 = + """ + \(let (bar = 15) bar + new { qux = 15 }.toString()) + """ + +prop3 = + """ + \(new { + // some comment + foo = 1 + + // some comment + bar = 2 + }) + """ + +prop4 = + """ + \(1 + /* block comment */ 2) + """ + +prop5 = + """ + \(""" + foo + bar + baz + """) + """ + +prop6 = + "\( // some line comment + /* some block comment */ + "\(""" + one + two + three + """)" + // some line comment again + )" + +prop7 = + "\(5 + // trailing line comment + )" + +prop8 = "\(new { foo = 1; bar = 2; baz = 3 })" diff --git a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java index 2dec2210..18eba2ab 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java +++ b/pkl-parser/src/main/java/org/pkl/parser/GenericParser.java @@ -965,7 +965,7 @@ public class GenericParser { case STRING_PART -> { var tk = next(); if (!tk.text(lexer).isEmpty()) { - children.add(make(NodeType.STRING_CONSTANT, tk.span)); + children.add(make(NodeType.STRING_CHARS, tk.span)); } } case STRING_ESCAPE_NEWLINE, @@ -1004,7 +1004,7 @@ public class GenericParser { case STRING_PART -> { var tk = next(); if (!tk.text(lexer).isEmpty()) { - children.add(make(NodeType.STRING_CONSTANT, tk.span)); + children.add(make(NodeType.STRING_CHARS, tk.span)); } } case STRING_NEWLINE -> children.add(make(NodeType.STRING_NEWLINE, next().span)); @@ -1383,7 +1383,7 @@ public class GenericParser { } } children.add(makeTerminal(next())); // string end - return new Node(NodeType.STRING_CONSTANT, children); + return new Node(NodeType.STRING_CHARS, children); } private FullToken expect(Token type, String errorKey, Object... messageArgs) { diff --git a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java index c898fdab..8fe7202e 100644 --- a/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java +++ b/pkl-parser/src/main/java/org/pkl/parser/syntax/generic/NodeType.java @@ -67,7 +67,7 @@ public enum NodeType { TYPE_ARGUMENT_LIST_ELEMENTS, OBJECT_PARAMETER_LIST, TYPE_PARAMETER, - STRING_CONSTANT, + STRING_CHARS, OPERATOR, STRING_NEWLINE, STRING_ESCAPE, diff --git a/pkl-parser/src/test/kotlin/org/pkl/parser/GenericSexpRenderer.kt b/pkl-parser/src/test/kotlin/org/pkl/parser/GenericSexpRenderer.kt index 2537dfe2..621cc706 100644 --- a/pkl-parser/src/test/kotlin/org/pkl/parser/GenericSexpRenderer.kt +++ b/pkl-parser/src/test/kotlin/org/pkl/parser/GenericSexpRenderer.kt @@ -130,7 +130,7 @@ class GenericSexpRenderer(code: String) { } private fun NodeType.isStringData(): Boolean = - this == NodeType.STRING_CONSTANT || this == NodeType.STRING_ESCAPE + this == NodeType.STRING_CHARS || this == NodeType.STRING_ESCAPE private fun name(node: Node): String = when (node.type) { @@ -142,7 +142,7 @@ class GenericSexpRenderer(code: String) { NodeType.EXTENDS_CLAUSE, NodeType.AMENDS_CLAUSE -> "extendsOrAmendsClause" NodeType.TYPEALIAS -> "typeAlias" - NodeType.STRING_ESCAPE -> "stringConstant" + NodeType.STRING_ESCAPE -> "stringChars" NodeType.READ_EXPR -> { val terminal = node.children.find { it.type == NodeType.TERMINAL }!!.text(source) when (terminal) { diff --git a/pkl-parser/src/test/kotlin/org/pkl/parser/SexpRenderer.kt b/pkl-parser/src/test/kotlin/org/pkl/parser/SexpRenderer.kt index 7d8e2cae..d6563a56 100644 --- a/pkl-parser/src/test/kotlin/org/pkl/parser/SexpRenderer.kt +++ b/pkl-parser/src/test/kotlin/org/pkl/parser/SexpRenderer.kt @@ -430,7 +430,7 @@ class SexpRenderer { renderExpr(part.expr) } else { buf.append('\n').append(tab) - buf.append("(stringConstant)") + buf.append("(stringChars)") } } buf.append(')') @@ -736,7 +736,7 @@ class SexpRenderer { fun renderStringConstant(str: StringConstant) { buf.append(tab) - buf.append("(stringConstant)") + buf.append("(stringChars)") } fun renderTypeAnnotation(typeAnnotation: TypeAnnotation) {