Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,44 @@ docstrings.oneline = unfold
val a = 1
```

#### `docstrings.wrap`

Will parse scaladoc comments and reformat them.

> This functionality is generally limited to
> [standard scaladoc elements](https://docs.scala-lang.org/overviews/scaladoc/for-library-authors.html)
> and might lead to undesirable results in corner cases;
> for instance, the scaladoc parser doesn't have proper support of embedded HTML.
Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@olafurpg would you like to amend this statement to say it will be treated as normal text?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good 👍

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perfect, i will merge.

separately, i will add table support to scalameta and then propagate it here. does this sound reasonable: https://www.scala-lang.org/blog/2018/10/04/scaladoc-tables.html


```scala mdoc:defaults
docstrings.wrap
```

```scala mdoc:scalafmt
docstrings.wrap = yes
maxColumn = 30
---
/**
* @param d the Double to square, meaning multiply by itself
* @return the result of squaring d
*
* Thus
* - if [[d]] represents a negative value:
* a. the result will be positive
* a. the value will be {{{d * d}}}
* a. it will be the same as for `-d`
* - however, if [[d]] is positive
* - the value will still be {{{d * d}}}
* - i.e., the same as {{{(-d) * (-d)}}}
*
* In other words:
* {{{
* res = d * d
* = (-d) * (-d) }}}
*/
def pow2(d: Double): Double
```

### `assumeStandardLibraryStripMargin`

This parameter simply says the `.stripMargin` method was not redefined
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import metaconfig._
* @param oneline
* - if fold, try to fold short docstrings into a single line
* - if unfold, unfold a single-line docstring into multiple lines
* @param wrap
* if yes, allow reformatting/rewrapping the contents of the docstring
* @param style
* - preserve: keep existing formatting
* - Asterisk: format intermediate lines with an asterisk below the
Expand All @@ -17,6 +19,7 @@ import metaconfig._
*/
case class Docstrings(
oneline: Docstrings.Oneline = Docstrings.Oneline.keep,
wrap: Docstrings.Wrap = Docstrings.Wrap.no,
style: Option[Docstrings.Style] = Some(Docstrings.SpaceAsterisk)
) {
import Docstrings._
Expand Down Expand Up @@ -68,4 +71,12 @@ object Docstrings {
ReaderUtil.oneOf[Oneline](keep, fold, unfold)
}

sealed abstract class Wrap
object Wrap {
case object no extends Wrap
case object yes extends Wrap
implicit val codec: ConfCodec[Wrap] =
ReaderUtil.oneOf[Wrap](no, yes)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import org.scalafmt.util.{LiteralOps, TreeOps}
import scala.annotation.tailrec
import scala.collection.AbstractIterator
import scala.collection.mutable
import scala.meta.internal.Scaladoc
import scala.meta.internal.parsers.ScaladocParser
import scala.meta.tokens.Token
import scala.meta.tokens.{Token => T}
import scala.meta.transversers.Traverser
Expand Down Expand Up @@ -426,52 +428,21 @@ class FormatWriter(formatOps: FormatOps) {
text: String
)(implicit sb: StringBuilder): Unit = {
if (style.docstrings.style.isEmpty) sb.append(text)
else if (!formatOnelineDocstring(text)) formatMultilineDocstring(text)
else if (!formatOnelineDocstring(text))
new FormatMlDoc(text).format
}

private def formatMultilineDocstring(
text: String
)(implicit sb: StringBuilder): Unit = {
val isExtraSpace = style.docstrings.isAsteriskSpace
val extraIndent = if (style.docstrings.isSpaceAsterisk) 2 else 1
val spaces: String = getIndentation(prevState.indentation + extraIndent)
// remove "/**" and "*/"
val trimmed = CharBuffer.wrap(text, 3, text.length - 2)
val matcher = docstringLine.matcher(trimmed)
def appendLineBreak = sb.append('\n').append(spaces).append('*')
sb.append("/**")
val sbLen = sb.length()
var prevWasBlank = style.docstrings.isAsterisk
while (matcher.find()) {
val contentBeg = matcher.start(2)
val contentEnd = matcher.end(2)
if (contentBeg == contentEnd) prevWasBlank = true
else {
if (sb.length() != sbLen) appendLineBreak
if (prevWasBlank) {
appendLineBreak
prevWasBlank = false
}
val leadSpaces = matcher.end(1) - matcher.start(1)
val minSpaces = if (isExtraSpace && sb.length() != sbLen) 2 else 1
sb.append(getIndentation(math.max(minSpaces, leadSpaces)))
sb.append(CharBuffer.wrap(trimmed, contentBeg, contentEnd))
}
}
if (!prevWasBlank) sb.append(" */")
else {
appendLineBreak
sb.append('/')
}
}

private abstract class FormatCommentBase(implicit sb: StringBuilder) {
private abstract class FormatCommentBase(
protected val extraIndent: Int = 1,
protected val leadingMargin: Int = 0
)(implicit sb: StringBuilder) {
protected final val breakBefore = curr.hasBreakBefore
protected final val indent =
if (breakBefore) prevState.indentation
else prevState.prev.indentation
// 2 is for "/*" or " *" or "//"
protected final val maxLength = style.maxColumn - indent - 2
// extra 1 is for "*" (in "/*" or " *") or "/" (in "//")
protected final val maxLength =
style.maxColumn - indent - extraIndent - 1

protected final def getFirstLineLength =
if (breakBefore) 0
Expand All @@ -497,7 +468,8 @@ class FormatWriter(formatOps: FormatOps) {
if (iter.hasNext) {
val word = iter.next()
val length = word.length
val maybeNextLineLength = lineLength + length + 1
val maybeNextLineLength = 1 + length +
(if (lineLength == 0) leadingMargin else lineLength)
val nextLineLength =
if (
lineLength < extraMargin.length ||
Expand Down Expand Up @@ -616,6 +588,145 @@ class FormatWriter(formatOps: FormatOps) {
}
}

private class FormatMlDoc(text: String)(implicit sb: StringBuilder)
extends FormatCommentBase(
if (style.docstrings.isSpaceAsterisk) 2 else 1,
if (style.docstrings.isAsteriskSpace) 1 else 0
) {
private val spaces: String = getIndentation(indent + extraIndent)
private val margin = getIndentation(1 + leadingMargin)

def format: Unit = {
val docOpt =
if (
(style.docstrings.wrap eq Docstrings.Wrap.yes) &&
curr.isStandalone
)
ScaladocParser.parse(tok.left.syntax)
else None
docOpt.fold(formatNoWrap)(formatWithWrap)
}

private def formatWithWrap(doc: Scaladoc): Unit = {
sb.append("/**")
if (style.docstrings.isAsterisk) appendBreak()
sb.append(' ')
val sbLen = sb.length()
val paras = doc.para.iterator
paras.foreach { para =>
para.term.foreach { term =>
if (sb.length() != sbLen) sb.append(margin)
term match {
case t: Scaladoc.CodeBlock =>
sb.append("{{{")
appendBreak()
t.code.foreach { x =>
val matcher = docstringLeadingSpace.matcher(x)
if (!matcher.lookingAt())
sb.append(getIndentation(2 + margin.length)).append(x)
else {
val offset = matcher.end()
val codeIndent =
math.max(margin.length, offset - offset % 2)
sb.append(getIndentation(codeIndent))
sb.append(CharBuffer.wrap(x, offset, x.length))
}
appendBreak()
}
sb.append(margin).append("}}}")
appendBreak()
case t: Scaladoc.Heading =>
val delimiter = t.level * '='
sb.append(delimiter).append(t.title).append(delimiter)
appendBreak()
case t: Scaladoc.Tag =>
sb.append(t.tag.tag)
if (t.tag.hasLabel) sb.append(' ').append(t.label.syntax)
if (t.tag.hasDesc) {
val words = t.desc.parts.iterator.map(_.syntax)
val tagMargin = getIndentation(2 + margin.length)
// use maxLength to force a newline
iterWords(words, appendBreak, maxLength, tagMargin)
}
appendBreak()
case t: Scaladoc.ListBlock =>
// outputs margin space and appends new line, too
// therefore, let's start by "rewinding"
sb.setLength(sb.length() - margin.length)
formatListBlock(getIndentation(margin.length + 2))(t)
case t: Scaladoc.Text =>
formatTextAfterMargin(t.parts.iterator.map(_.syntax))
case Scaladoc.Unknown(t) =>
formatTextAfterMargin(splitAsIterator(docstringSpace)(t))
}
}
if (paras.hasNext) appendBreak()
}
sb.append('/')
}

private def formatTextAfterMargin(words: WordIter): Unit = {
// remove space as iterWords adds it
sb.setLength(sb.length() - 1)
iterWords(words, appendBreak, 0, margin)
appendBreak()
}

private def formatListBlock(
listIndent: String
)(block: Scaladoc.ListBlock): Unit = {
val prefix = block.prefix
val itemIndent = getIndentation(listIndent.length + prefix.length + 1)
block.items.foreach { x =>
sb.append(listIndent).append(prefix)
formatListTerm(itemIndent)(x)
}
}

private def formatListTerm(
itemIndent: String
)(item: Scaladoc.ListItem): Unit = {
val words = item.text.parts.iterator.map(_.syntax)
iterWords(words, appendBreak, itemIndent.length - 1, itemIndent)
appendBreak()
item.nested.foreach(formatListBlock(itemIndent))
}

private def formatNoWrap: Unit = {
// remove "/**" and "*/"
val trimmed = CharBuffer.wrap(text, 3, text.length - 2)
val matcher = docstringLine.matcher(trimmed)
sb.append("/**")
val sbLen = sb.length()
var prevWasBlank = style.docstrings.isAsterisk
while (matcher.find()) {
val contentBeg = matcher.start(2)
val contentEnd = matcher.end(2)
if (contentBeg == contentEnd) prevWasBlank = true
else {
if (sb.length() != sbLen) appendBreak()
if (prevWasBlank) {
appendBreak
prevWasBlank = false
}
if (sb.length() == sbLen) sb.append(' ') else sb.append(margin)
val extraMargin =
matcher.end(1) - matcher.start(1) - margin.length
if (extraMargin > 0) sb.append(getIndentation(extraMargin))
sb.append(CharBuffer.wrap(trimmed, contentBeg, contentEnd))
}
}
if (!prevWasBlank) sb.append(" */")
else {
appendBreak
sb.append('/')
}
}

private def appendBreak(): Unit =
sb.append('\n').append(spaces).append('*')
}

}

/**
Expand Down Expand Up @@ -1151,6 +1262,8 @@ object FormatWriter {
private val onelineDocstring = Pattern.compile(
"^/\\*\\*(?:\n\\h*+\\*?)?\\h*+([^*][^\n]*[^\n\\h])(?:\n\\h*+\\**?)?\\h*+\\*/$"
)
private val docstringSpace = Pattern.compile("[\\h\n]++")
private val docstringLeadingSpace = Pattern.compile("^\\h*+")

@inline
private def getStripMarginPattern(pipe: Char) =
Expand Down
Loading