-
Notifications
You must be signed in to change notification settings - Fork 1
Add Doc.align that aligns subsequent line breaks to the current line position
#155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5f846ce
699c6f9
006e6d4
2b26dc4
7e33de9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: © 2022-2024 Opencast Software Europe Ltd <https://opencastsoftware.com> | ||
| * SPDX-FileCopyrightText: © 2022-2025 Opencast Software Europe Ltd <https://opencastsoftware.com> | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package com.opencastsoftware.prettier4j; | ||
|
|
@@ -236,6 +236,15 @@ public Doc indent(int indent) { | |
| return indent(indent, this); | ||
| } | ||
|
|
||
| /** | ||
| * Align any line breaks within this {@link Doc} to the line position at the start of the {@link Doc}. | ||
| * | ||
| * @return the aligned document. | ||
| */ | ||
| public Doc align() { | ||
| return align(this); | ||
| } | ||
|
|
||
| /** | ||
| * Bracket the current document by the {@code left} and {@code right} Strings, | ||
| * indented by {@code indent} spaces. | ||
|
|
@@ -849,6 +858,67 @@ public String toString() { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Represents an aligned {@link Doc}. | ||
| * | ||
| * Sets the indentation for line breaks within its inner {@link Doc} at the current line position. | ||
| */ | ||
| public static class Align extends Doc { | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It may also be worth adding an absolute |
||
| private final Doc doc; | ||
|
|
||
| Align(Doc doc) { | ||
| this.doc = doc; | ||
| } | ||
|
|
||
| public Doc doc() { | ||
| return doc; | ||
| } | ||
|
|
||
| @Override | ||
| Doc flatten() { | ||
| return new Align(doc.flatten()); | ||
| } | ||
|
|
||
| @Override | ||
| boolean hasParams() { | ||
| return doc.hasParams(); | ||
| } | ||
|
|
||
| @Override | ||
| boolean hasLineSeparators() { | ||
| return doc.hasLineSeparators(); | ||
| } | ||
|
|
||
| @Override | ||
| public Doc bind(String name, Doc value) { | ||
| return new Align(doc.bind(name, value)); | ||
| } | ||
|
|
||
| @Override | ||
| public Doc bind(Map<String, Doc> bindings) { | ||
| return new Align(doc.bind(bindings)); | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
| Align align = (Align) o; | ||
| return Objects.equals(doc, align.doc); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hashCode(doc); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "Align[" + | ||
| "doc=" + doc + | ||
| ']'; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Represents a line break which cannot be flattened into a more compact layout. | ||
| */ | ||
|
|
@@ -1615,6 +1685,16 @@ public static Doc indent(int indent, Doc doc) { | |
| return new Indent(indent, doc); | ||
| } | ||
|
|
||
| /** | ||
| * Align any line breaks within this {@link Doc} to the line position at the start of the {@link Doc}. | ||
| * | ||
| * @param doc the input document | ||
| * @return the aligned document. | ||
| */ | ||
| public static Doc align(Doc doc) { | ||
| return new Align(doc); | ||
| } | ||
|
|
||
| /** | ||
| * Apply the margin document {@code margin} to the current {@link Doc}, emitting the | ||
| * margin at the start of every new line from the start of this document until the | ||
|
|
@@ -2073,6 +2153,10 @@ private static int layoutEntry(RenderOptions options, Deque<Entry> inQueue, Queu | |
| Indent indentDoc = (Indent) entryDoc; | ||
| int newIndent = entryIndent + indentDoc.indent(); | ||
| inQueue.addFirst(entry(newIndent, entryMargin, indentDoc.doc())); | ||
| } else if (entryDoc instanceof Align) { | ||
| // Eliminate Align | ||
| Align alignDoc = (Align) entryDoc; | ||
| inQueue.addFirst(entry(position, entryMargin, alignDoc.doc())); | ||
| } else if (entryDoc instanceof Margin) { | ||
| // Eliminate Margin | ||
| Margin marginDoc = (Margin) entryDoc; | ||
|
|
@@ -2097,7 +2181,7 @@ private static int layoutEntry(RenderOptions options, Deque<Entry> inQueue, Queu | |
| outQueue.add(topEntry); | ||
| } else if (entryDoc instanceof LineOr) { | ||
| // Reset line length | ||
| position = entryIndent; | ||
| position = 0; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is based on this observation that sending out the indent text below already manipulates the position to add the I missed this while implementing |
||
| // Note reverse order | ||
| if (entryIndent > 0) { | ||
| // Send out the indent spaces | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| /* | ||
| * SPDX-FileCopyrightText: © 2022-2024 Opencast Software Europe Ltd <https://opencastsoftware.com> | ||
| * SPDX-FileCopyrightText: © 2022-2025 Opencast Software Europe Ltd <https://opencastsoftware.com> | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| package com.opencastsoftware.prettier4j; | ||
|
|
@@ -21,10 +21,10 @@ | |
| import java.io.StringWriter; | ||
| import java.io.Writer; | ||
| import java.net.URI; | ||
| import java.util.Arrays; | ||
| import java.util.Collections; | ||
| import java.util.*; | ||
| import java.util.function.UnaryOperator; | ||
| import java.util.stream.Collectors; | ||
| import java.util.stream.Stream; | ||
|
|
||
| import static com.opencastsoftware.prettier4j.Doc.*; | ||
| import static org.hamcrest.MatcherAssert.assertThat; | ||
|
|
@@ -113,6 +113,15 @@ void testAppendLineFlattening() { | |
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testAppendLineWithAlign() { | ||
| String expected = "one two\n three"; | ||
| String actual = text("one") | ||
| .appendSpace(group(align(text("two").appendLine(text("three"))))) | ||
| .render(30); | ||
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testAppendLineOrSpace() { | ||
| String expected = "one two three"; | ||
|
|
@@ -123,6 +132,15 @@ void testAppendLineOrSpace() { | |
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testAppendLineOrSpaceWithAlign() { | ||
| String expected = "one two three"; | ||
| String actual = text("one") | ||
| .appendSpace(group(align(text("two").appendLineOrSpace(text("three"))))) | ||
| .render(30); | ||
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testAppendLineOrSpaceFlattening() { | ||
| String expected = "one\ntwo\nthree"; | ||
|
|
@@ -133,6 +151,15 @@ void testAppendLineOrSpaceFlattening() { | |
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testAppendLineOrSpaceWithAlignFlattening() { | ||
| String expected = "one two\n three"; | ||
| String actual = text("one") | ||
| .appendSpace(group(align(text("two").appendLineOrSpace(text("three"))))) | ||
| .render(10); | ||
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testAppendLineOrEmpty() { | ||
| String expected = "onetwothree"; | ||
|
|
@@ -217,6 +244,132 @@ void testBracketFlattening() { | |
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testBracketFlatteningWithAlign() { | ||
| // Note: the arguments are aligned with the "functionCall" element because bracket doesn't support alignment. | ||
| // TODO: Consider adding a `hangingBracket` combinator that aligns the bracket docs with the starting line position | ||
| // and the arguments with the opening bracket doc. | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| String expected = "functionCall(\n"+ | ||
| Indents.get(12)+"a,\n"+ | ||
| Indents.get(12)+"b,\n"+ | ||
| Indents.get(12)+"c\n"+ | ||
| Indents.get(12)+")"; | ||
| String actual = text("functionCall") | ||
| .append( | ||
| Doc.intersperse( | ||
| Doc.text(",").append(Doc.lineOrSpace()), | ||
| Stream.of("a", "b", "c").map(Doc::text)) | ||
| .bracket(0, Doc.lineOrEmpty(), Doc.text("("), Doc.text(")")) | ||
| .align()) | ||
| .render(10); | ||
|
|
||
| assertThat(actual, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testNestedBracketFlattening() { | ||
| String expectedWidth80 = "let x = functionCall(with, args, nestedFunctionCall(with, more, args))"; | ||
| String expectedWidth40 = "let x = functionCall(\n with,\n args,\n nestedFunctionCall(with, more, args)\n)"; | ||
| String expectedWidth20 = "let x = functionCall(\n with,\n args,\n nestedFunctionCall(\n with,\n more,\n args\n )\n)"; | ||
|
|
||
| Doc inputDoc = text("let") | ||
| .appendSpace(text("x")) | ||
| .appendSpace(text("=")) | ||
| .appendSpace(text("functionCall") | ||
| .append( | ||
| intersperse( | ||
| text(",").append(lineOrSpace()), | ||
| Stream.concat( | ||
| Stream.of("with", "args").map(Doc::text), | ||
| Stream.of(text("nestedFunctionCall") | ||
| .append( | ||
| intersperse( | ||
| text(",").append(lineOrSpace()), | ||
| Stream.of("with", "more", "args").map(Doc::text) | ||
| ).bracket(2, lineOrEmpty(), text("("), text(")")) | ||
| )) | ||
| ) | ||
| ).bracket(2, lineOrEmpty(), text("("), text(")")) | ||
| ) | ||
| ); | ||
|
|
||
| assertThat(inputDoc.render(80), is(equalTo(expectedWidth80))); | ||
| assertThat(inputDoc.render(40), is(equalTo(expectedWidth40))); | ||
| assertThat(inputDoc.render(20), is(equalTo(expectedWidth20))); | ||
| } | ||
|
|
||
| @Test | ||
| void testNestedBracketFlatteningWithAlign() { | ||
| String expectedWidth80 = "let x = functionCall(with, args, nestedFunctionCall(with, more, args))"; | ||
| String expectedWidth40 = | ||
| "let x = functionCall(\n"+ | ||
| Indents.get(20)+"with,\n"+ | ||
| Indents.get(20)+"args,\n"+ | ||
| Indents.get(20)+"nestedFunctionCall(\n"+ | ||
| Indents.get(38)+"with,\n"+ | ||
| Indents.get(38)+"more,\n"+ | ||
| Indents.get(38)+ "args\n"+ | ||
| Indents.get(38)+")\n"+ | ||
| Indents.get(20)+")"; | ||
|
|
||
| Doc inputDoc = text("let") | ||
| .appendSpace(text("x")) | ||
| .appendSpace(text("=")) | ||
| .appendSpace(text("functionCall") | ||
| .append(align( | ||
| intersperse( | ||
| text(",").append(lineOrSpace()), | ||
| Stream.concat( | ||
| Stream.of("with", "args").map(Doc::text), | ||
| Stream.of(text("nestedFunctionCall") | ||
| .append(align( | ||
| intersperse( | ||
| text(",").append(lineOrSpace()), | ||
| Stream.of("with", "more", "args").map(Doc::text) | ||
| ).bracket(0, lineOrEmpty(), text("("), text(")")) | ||
| ))) | ||
| ) | ||
| ).bracket(0, lineOrEmpty(), text("("), text(")")) | ||
| )) | ||
| ); | ||
|
|
||
| assertThat(inputDoc.render(80), is(equalTo(expectedWidth80))); | ||
| assertThat(inputDoc.render(40), is(equalTo(expectedWidth40))); | ||
| } | ||
|
|
||
| @Test | ||
| void testAlignWithMultipleLines() { | ||
| String expected = | ||
| "∧ ∨ A ∨ B\n" + | ||
| " ∨ C\n" + | ||
| "∧ ∨ D\n" + | ||
| " ∨ E ∧ F\n" + | ||
| " ∨ G"; | ||
|
|
||
| // (A ∨ B) ∨ C | ||
| List<Doc> left = List.of( | ||
| text("A").appendSpace(text("∨")).appendSpace(text("B")), | ||
| text("C") | ||
| ); | ||
|
|
||
| // D ∨ (E ∧ F) ∨ G | ||
| List<Doc> right = List.of( | ||
| text("D"), | ||
| text("E").appendSpace(text("∧")).appendSpace(text("F")), | ||
| text("G") | ||
| ); | ||
|
|
||
| Doc alignedLeft = align(text("∨").appendSpace(intersperse(line().append(text("∨ ")), left))); | ||
| Doc leftJunctions = text("∧").appendSpace(alignedLeft); | ||
|
|
||
| Doc alignedRight = align(text("∨").appendSpace(intersperse(line().append(text("∨ ")), right))); | ||
| Doc rightJunctions = text("∧").appendSpace(alignedRight); | ||
|
|
||
| String result = leftJunctions.appendLine(rightJunctions).render(80); | ||
|
|
||
| assertThat(result, is(equalTo(expected))); | ||
| } | ||
|
|
||
| @Test | ||
| void testMarginWithLineSeparator() { | ||
| assertThrows(IllegalArgumentException.class, () -> { | ||
|
|
@@ -1549,7 +1702,7 @@ void testEquals() { | |
| EqualsVerifier | ||
| .forClasses( | ||
| Text.class, Append.class, Param.class, WrapText.class, | ||
| Alternatives.class, Indent.class, Margin.class, Link.class, | ||
| Alternatives.class, Indent.class, Align.class, Margin.class, Link.class, | ||
| LineOr.class, Escape.class, Styled.class, OpenLink.class) | ||
| .usingGetClass() | ||
| .withPrefabValues(Doc.class, left, right) | ||
|
|
@@ -1562,7 +1715,7 @@ void testToString() { | |
| .forClasses( | ||
| Text.class, Append.class, Margin.class, | ||
| WrapText.class, Alternatives.class, Indent.class, | ||
| LineOr.class, Empty.class, Escape.class, | ||
| LineOr.class, Empty.class, Escape.class, Align.class, | ||
| Link.class, OpenLink.class, CloseLink.class, | ||
| Reset.class, Styled.class, Param.class) | ||
| .withPrefabValue(Doc.class, docsWithParams().sample()) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit wary of adding too much stuff to this library, but it's possible to get "hanging" style indent by adding a variant of
bracketwith the following implementation: