diff --git a/docs/configuration.md b/docs/configuration.md
index b53f6b69db..bf34c6a913 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -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.
+
+```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
diff --git a/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Docstrings.scala b/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Docstrings.scala
index 2bc8629ba2..6bc39f0f7e 100644
--- a/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Docstrings.scala
+++ b/scalafmt-core/shared/src/main/scala/org/scalafmt/config/Docstrings.scala
@@ -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
@@ -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._
@@ -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)
+ }
+
}
diff --git a/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala b/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
index 9d61f4579e..622564c97d 100644
--- a/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
+++ b/scalafmt-core/shared/src/main/scala/org/scalafmt/internal/FormatWriter.scala
@@ -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
@@ -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
@@ -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 ||
@@ -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('*')
+ }
+
}
/**
@@ -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) =
diff --git a/scalafmt-tests/src/test/resources/test/JavaDoc.stat b/scalafmt-tests/src/test/resources/test/JavaDoc.stat
index 85fa6a3b96..202e031bd1 100644
--- a/scalafmt-tests/src/test/resources/test/JavaDoc.stat
+++ b/scalafmt-tests/src/test/resources/test/JavaDoc.stat
@@ -193,3 +193,493 @@ class Main
* Main entry-point.
*/
class Main
+<<< #1387 1 Asterisk
+docstrings.wrap = yes
+docstrings.style = Asterisk
+maxColumn = 30
+===
+/** Start the comment here
+ *
+ * @param d the Double to square, meaning multiply by itself
+ * @returns the result of squaring d
+
+{{{
+multi
+line
+code
+}}}
+@tparam t type, @see [[d]] ({{{single-line code}}})
+ */
+ val a = 1
+>>>
+/**
+ * Start the comment here
+ *
+ * @param d
+ * the Double to square,
+ * meaning multiply by
+ * itself
+ * @returns
+ * the result of squaring d
+ *
+ * {{{
+ * multi
+ * line
+ * code
+ * }}}
+ * @tparam t
+ * type, @see [[d]]
+ * ({{{single-line code}}})
+ */
+val a = 1
+<<< #1387 1 SpaceAsterisk
+docstrings.wrap = yes
+docstrings.style = SpaceAsterisk
+maxColumn = 30
+===
+/** Start the comment here
+ *
+ * @param d the Double to square, meaning multiply by itself
+ * @returns the result of squaring d
+
+{{{
+multi
+line
+code
+}}}
+@tparam t type, @see [[d]] ({{{single-line code}}})
+ */
+ val a = 1
+>>>
+/** Start the comment here
+ *
+ * @param d
+ * the Double to square,
+ * meaning multiply by
+ * itself
+ * @returns
+ * the result of squaring d
+ *
+ * {{{
+ * multi
+ * line
+ * code
+ * }}}
+ * @tparam t
+ * type, @see [[d]]
+ * ({{{single-line code}}})
+ */
+val a = 1
+<<< #1387 1 AsteriskSpace
+docstrings.wrap = yes
+docstrings.style = AsteriskSpace
+maxColumn = 30
+===
+/** Start the comment here
+ *
+ * @param d the Double to square, meaning multiply by itself
+ * @returns the result of squaring d
+
+{{{
+multi
+line
+code
+}}}
+@tparam t type, @see [[d]] ({{{single-line code}}})
+ */
+ val a = 1
+>>>
+/** Start the comment here
+ *
+ * @param d
+ * the Double to square,
+ * meaning multiply by
+ * itself
+ * @returns
+ * the result of squaring d
+ *
+ * {{{
+ * multi
+ * line
+ * code
+ * }}}
+ * @tparam t
+ * type, @see [[d]]
+ * ({{{single-line code}}})
+ */
+val a = 1
+<<< #1887 1 Asterisk
+docstrings.wrap = yes
+docstrings.style = Asterisk
+maxColumn = 30
+===
+/** Start the comment here
+ *
+ * @inheritdoc
+ * some text1 some text2 some text3 some text4
+ * 1. list1 list2 list3 list4 list5
+ * - sublist1 sublist2 sublist3
+ * a. subsublist1 subsublist2
+ * - foolist1 foolist2 foolist3
+ * 1. barlist1 barlist2 barlist3
+ */
+ val a = 1
+>>>
+/**
+ * Start the comment here
+ *
+ * @inheritdoc
+ * some text1 some text2 some
+ * text3 some text4
+ * 1. list1 list2 list3
+ * list4 list5
+ * - sublist1 sublist2
+ * sublist3
+ * a. subsublist1
+ * subsublist2
+ * - foolist1 foolist2
+ * foolist3
+ * 1. barlist1 barlist2
+ * barlist3
+ */
+val a = 1
+<<< #1887 1 SpaceAsterisk
+docstrings.wrap = yes
+docstrings.style = SpaceAsterisk
+maxColumn = 30
+===
+/** Start the comment here
+ *
+ * @inheritdoc
+ * some text1 some text2 some text3 some text4
+ * 1. list1 list2 list3 list4 list5
+ * - sublist1 sublist2 sublist3
+ * a. subsublist1 subsublist2
+ * - foolist1 foolist2 foolist3
+ * 1. barlist1 barlist2 barlist3
+ */
+ val a = 1
+>>>
+/** Start the comment here
+ *
+ * @inheritdoc
+ * some text1 some text2 some
+ * text3 some text4
+ * 1. list1 list2 list3
+ * list4 list5
+ * - sublist1 sublist2
+ * sublist3
+ * a. subsublist1
+ * subsublist2
+ * - foolist1 foolist2
+ * foolist3
+ * 1. barlist1 barlist2
+ * barlist3
+ */
+val a = 1
+<<< #1887 1 AsteriskSpace
+docstrings.wrap = yes
+docstrings.style = AsteriskSpace
+maxColumn = 30
+===
+/** Start the comment here
+ *
+ * @inheritdoc
+ * some text1 some text2 some text3 some text4
+ * 1. list1 list2 list3 list4 list5
+ * - sublist1 sublist2 sublist3
+ * a. subsublist1 subsublist2
+ * - foolist1 foolist2 foolist3
+ * 1. barlist1 barlist2 barlist3
+ */
+ val a = 1
+>>>
+/** Start the comment here
+ *
+ * @inheritdoc
+ * some text1 some text2 some
+ * text3 some text4
+ * 1. list1 list2 list3
+ * list4 list5
+ * - sublist1 sublist2
+ * sublist3
+ * a. subsublist1
+ * subsublist2
+ * - foolist1 foolist2
+ * foolist3
+ * 1. barlist1 barlist2
+ * barlist3
+ */
+val a = 1
+<<< #1887 2 Asterisk: first line max
+docstrings.wrap = yes
+docstrings.style = Asterisk
+maxColumn = 30
+===
+object a {
+ /** Binds values which should
+ * be wrapped */
+}
+>>>
+object a {
+
+ /**
+ * Binds values which should
+ * be wrapped
+ */
+}
+<<< #1887 2 SpaceAsterisk: first line max
+docstrings.wrap = yes
+docstrings.style = SpaceAsterisk
+maxColumn = 30
+===
+object a {
+ /** Binds values which should
+ * be wrapped */
+}
+>>>
+object a {
+
+ /** Binds values which
+ * should be wrapped
+ */
+}
+<<< #1887 2 AsteriskSpace: first line max
+docstrings.wrap = yes
+docstrings.style = AsteriskSpace
+maxColumn = 30
+===
+object a {
+ /** Binds values which should
+ * be wrapped */
+}
+>>>
+object a {
+
+ /** Binds values which
+ * should be wrapped
+ */
+}
+<<< #1887 3 Asterisk: markdown
+docstrings.wrap = yes
+docstrings.style = Asterisk
+maxColumn = 15
+# https://docs.scala-lang.org/overviews/scaladoc/for-library-authors.html#markup
+===
+object a {
+ /**
+ `mono space`
+ ''italic text''
+ '''bold text'''
+ __under line__
+ ^super script^
+ ,,sub script,,
+ 'single quoted'
+ "double quoted"
+ */
+}
+>>>
+object a {
+
+ /**
+ * `mono
+ * space`
+ * ''italic
+ * text''
+ * '''bold
+ * text'''
+ * __under
+ * line__
+ * ^super
+ * script^
+ * ,,sub
+ * script,,
+ * 'single
+ * quoted'
+ * "double
+ * quoted"
+ */
+}
+<<< #1887 3 SpaceAsterisk: markdown
+docstrings.wrap = yes
+docstrings.style = SpaceAsterisk
+maxColumn = 15
+===
+object a {
+ /**
+ `mono space`
+ ''italic text''
+ '''bold text'''
+ __under line__
+ ^super script^
+ ,,sub script,,
+ 'single quoted'
+ "double quoted"
+ */
+}
+>>>
+object a {
+
+ /** `mono
+ * space`
+ * ''italic
+ * text''
+ * '''bold
+ * text'''
+ * __under
+ * line__
+ * ^super
+ * script^
+ * ,,sub
+ * script,,
+ * 'single
+ * quoted'
+ * "double
+ * quoted"
+ */
+}
+<<< #1887 3 AsteriskSpace: markdown
+docstrings.wrap = yes
+docstrings.style = AsteriskSpace
+maxColumn = 15
+===
+object a {
+ /**
+ `mono space`
+ ''italic text''
+ '''bold text'''
+ __under line__
+ ^super script^
+ ,,sub script,,
+ 'single quoted'
+ "double quoted"
+ */
+}
+>>>
+object a {
+
+ /** `mono
+ * space`
+ * ''italic
+ * text''
+ * '''bold
+ * text'''
+ * __under
+ * line__
+ * ^super
+ * script^
+ * ,,sub
+ * script,,
+ * 'single
+ * quoted'
+ * "double
+ * quoted"
+ */
+}
+<<< #1887 4 Asterisk: urls
+docstrings.wrap = yes
+docstrings.style = Asterisk
+maxColumn = 23
+===
+object a {
+ /** go to
+ http:/a.b.c/d . good luck.
+ */
+}
+>>>
+object a {
+
+ /**
+ * go to
+ * http:/a.b.c/d .
+ * good luck.
+ */
+}
+<<< #1887 4 SpaceAsterisk: urls
+docstrings.wrap = yes
+docstrings.style = SpaceAsterisk
+maxColumn = 24
+===
+object a {
+ /** go to
+ http:/a.b.c/d . good luck.
+ */
+}
+>>>
+object a {
+
+ /** go to
+ * http:/a.b.c/d .
+ * good luck.
+ */
+}
+<<< #1887 4 AsteriskSpace: urls
+docstrings.wrap = yes
+docstrings.style = AsteriskSpace
+maxColumn = 24
+===
+object a {
+ /** go to
+ http:/a.b.c/d . good luck.
+ */
+}
+>>>
+object a {
+
+ /** go to
+ * http:/a.b.c/d .
+ * good luck.
+ */
+}
+<<< #1887 5 Asterisk: html
+docstrings.wrap = yes
+docstrings.style = Asterisk
+maxColumn = 30
+===
+object a {
+ /** go to this url
+ */
+}
+>>>
+object a {
+
+ /**
+ * go to this
+ * url
+ */
+}
+<<< #1887 5 SpaceAsterisk: html
+docstrings.wrap = yes
+docstrings.style = SpaceAsterisk
+maxColumn = 30
+===
+object a {
+ /** go to this url
+ */
+}
+>>>
+object a {
+
+ /** go to this
+ * url
+ */
+}
+<<< #1887 5 AsteriskSpace: html
+docstrings.wrap = yes
+docstrings.style = AsteriskSpace
+maxColumn = 30
+===
+object a {
+ /** go to this url
+ */
+}
+>>>
+object a {
+
+ /** go to this
+ * url
+ */
+}