typelevel / paiges

an implementation of Wadler's a prettier printer
Apache License 2.0
186 stars 30 forks source link

How to print curly brace if it doesn't fit? #628

Open steinybot opened 1 month ago

steinybot commented 1 month ago

I would like to do:

def foo = bar

if it fits, otherwise do:

def foo = {
  bar
}

Is this possible?

Seems like I need FlatAlt("{" +: Doc.hardLine, doc) but there is no way to create a custom FlatAlt.

steinybot commented 1 month ago

Ohhh I can use Doc.fill.

Doc.fill("{" +: Doc.line, Seq(Doc.text("def foo ="), Doc.text("bar")))

Man this stuff takes me ages to get my head around. I've literally been staring at it for hours.

steinybot commented 1 month ago

Wait no I'm being dumb. That will always add the {. Damn it.

steinybot commented 1 month ago

Well this does it although it uses private stuff. Is there a better way?

package org.typelevel.paiges

import org.typelevel.paiges.Doc.FlatAlt

object Docx {

    implicit class DocOps(val doc: Doc) extends AnyVal {

        def orEmpty: Doc =
            flatAlt(doc, Doc.empty)

        def bracketIfLong(left: Doc, leftSep: Doc, rightSep: Doc, right: Doc, indent: Int = 2): Doc =
            left + (((leftSep + Doc.hardLine).orEmpty + doc).nested(indent) + (Doc.hardLine + rightSep).orEmpty + right).grouped
    }

    def bracketIfLong(left: Doc, leftSep: Doc, doc: Doc, rightSep: Doc, right: Doc, indent: Int = 2): Doc =
        doc.bracketIfLong(left, leftSep, rightSep, right, indent)

    def flatAlt(default: Doc, whenFlat: Doc): Doc =
        if (default == whenFlat) default
        else FlatAlt(default, whenFlat)
}
import org.scalatest.freespec.AnyFreeSpec
import org.typelevel.paiges.Doc
import org.typelevel.paiges.Docx

class MethodBuilderTest extends AnyFreeSpec {

    "MethodBuilder" - {
        "should add {} when the body is too long" in {
            val sig = Doc.text("def foo = ")
            val body = Doc.text("howlongcanyougo")
            val doc = Docx.bracketIfLong(sig, Doc.char('{'), body, Doc.char('}'), Doc.empty)
            val result = doc.render(24)
            val expected =
                """def foo = {
                  |  howlongcanyougo
                  |}""".stripMargin
            assert(result == expected)
        }
        "should not add {} when the body fits" in {
            val sig = Doc.text("def foo = ")
            val body = Doc.text("howlongcanyougo")
            val doc = Docx.bracketIfLong(sig, Doc.char('{'), body, Doc.char('}'), Doc.empty)
            val result = doc.render(25)
            val expected = """def foo = howlongcanyougo""".stripMargin
            assert(result == expected)
        }
    }
}
johnynek commented 1 month ago

This is a good solution.

Flatly was not directly exposed because it can break the invariants. It only makes sense for certain pairs of Docs. For instance if you flatten the resulting Doc should have lines as long or longer, not shorter.

That said, I think you .orEmpty combinator is safe and doesn't break invariants.

However, composing two such as you have done should only be done with a .grouped (the whole thing flattens or not at all) so a comment should be made around the method.

If you would make a PR I'd be happy to review and I think we could merge it.

steinybot commented 1 month ago

Flatly was not directly exposed because it can break the invariants. It only makes sense for certain pairs of Docs. For instance if you flatten the resulting Doc should have lines as long or longer, not shorter.

orEmpty in general would break the invariant width(default) <= width(whenFlat) wouldn't it?

So then I think bracketIfLong would be safe to use orEmpty if and only if width(leftSep) <= width(doc).

However, composing two such as you have done should only be done with a .grouped (the whole thing flattens or not at all) so a comment should be made around the method. Sorry, what should be grouped?

Are you suggesting a PR for just orEmpty or bracketIfLong too?