Skip to content

Commit

Permalink
Merge pull request #5394 from dotty-staging/topic/ide-better-doc
Browse files Browse the repository at this point in the history
IDE: Improve display of the documentation on hover
  • Loading branch information
Duhemm committed Nov 13, 2018
2 parents 6a315ff + cc1b7fa commit e27b6fa
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 59 deletions.
11 changes: 11 additions & 0 deletions compiler/src/dotty/tools/dotc/util/CommentParsing.scala
Expand Up @@ -5,6 +5,8 @@
*/
package dotty.tools.dotc.util

import scala.collection.mutable

/** The comment parsing in `dotc` is used by both the comment cooking and the
* dottydoc tool.
*
Expand Down Expand Up @@ -223,6 +225,15 @@ object CommentParsing {
result
}

/** A map from tag name to all boundaries for this tag */
def groupedSections(str: String, sections: List[(Int, Int)]): Map[String, List[(Int, Int)]] = {
val map = mutable.Map.empty[String, List[(Int, Int)]].withDefaultValue(Nil)
sections.reverse.foreach { bounds =>
val tag = extractSectionTag(str, bounds)
map.update(tag, (skipTag(str, bounds._1), bounds._2) :: map(tag))
}
map.toMap
}

def removeSections(raw: String, xs: String*): String = {
val sections = tagIndex(raw)
Expand Down
221 changes: 221 additions & 0 deletions compiler/src/dotty/tools/dotc/util/ParsedComment.scala
@@ -0,0 +1,221 @@
package dotty.tools.dotc.util

import dotty.tools.dotc.core.Comments.{Comment, CommentsContext}
import dotty.tools.dotc.core.Contexts.Context
import dotty.tools.dotc.core.Names.TermName
import dotty.tools.dotc.core.Symbols._
import dotty.tools.dotc.printing.SyntaxHighlighting

import scala.Console.{BOLD, RESET, UNDERLINED}
import scala.collection.immutable.ListMap
import scala.util.matching.Regex

/**
* A parsed doc comment.
*
* @param comment The doc comment to parse
*/
class ParsedComment(val comment: Comment) {

/**
* The bounds of a section that represents the [start; end[ char offset
* of the section within this comment's `content`.
*/
private type Bounds = (Int, Int)

/** The content of this comment, after expansion if possible. */
val content: String = comment.expandedBody.getOrElse(comment.raw)

/** An index that marks all sections boundaries */
private lazy val tagIndex: List[Bounds] = CommentParsing.tagIndex(content)

/**
* Maps a parameter name to the bounds of its doc
*
* @see paramDoc
*/
private lazy val paramDocs: Map[String, Bounds] = CommentParsing.paramDocs(content, "@param", tagIndex)

/**
* The "main" documentation for this comment. That is, the comment before any section starts.
*/
lazy val mainDoc: String = {
val doc = tagIndex match {
case Nil => content.stripSuffix("*/")
case (start, _) :: _ => content.slice(0, start)
}
clean(doc.stripPrefix("/**"))
}

/**
* Renders this comment as markdown.
*
* The different sections are formatted according to the mapping in `knownTags`.
*/
def renderAsMarkdown(implicit ctx: Context): String = {
val buf = new StringBuilder
buf.append(mainDoc + System.lineSeparator + System.lineSeparator)
val groupedSections = CommentParsing.groupedSections(content, tagIndex)

for {
(tag, formatter) <- ParsedComment.knownTags
boundss <- groupedSections.get(tag)
texts = boundss.map { case (start, end) => clean(content.slice(start, end)) }
formatted <- formatter(texts)
} {
buf.append(formatted)
buf.append(System.lineSeparator)
}

buf.toString
}

/**
* The `@param` section corresponding to `name`.
*
* @param name The parameter name whose documentation to extract.
* @return The formatted documentation corresponding to `name`.
*/
def paramDoc(name: TermName): Option[String] = paramDocs.get(name.toString).map { case (start, end) =>
val rawContent = content.slice(start, end)
val docContent = ParsedComment.prefixRegex.replaceFirstIn(rawContent, "")
clean(docContent)
}

/**
* Cleans `str`: remove prefixing `*` and trim the string.
*
* @param str The string to clean
* @return The cleaned string.
*/
private def clean(str: String): String = str.stripMargin('*').trim
}

object ParsedComment {

/**
* Return the `ParsedComment` associated with `symbol`, if it exists.
*
* @param symbol The symbol for which to retrieve the documentation
* @return If it exists, the `ParsedComment` for `symbol`.
*/
def docOf(symbol: Symbol)(implicit ctx: Context): Option[ParsedComment] = {
val documentedSymbol = if (symbol.isPrimaryConstructor) symbol.owner else symbol
for { docCtx <- ctx.docCtx
comment <- docCtx.docstring(documentedSymbol)
} yield new ParsedComment(comment)
}

@scala.annotation.internal.sharable
private val prefixRegex = """@param\s+\w+\s+""".r

/** A mapping from tag name to `TagFormatter` */
private val knownTags: ListMap[String, TagFormatter] = ListMap(
"@tparam" -> TagFormatter("Type Parameters", toDescriptionList),
"@param" -> TagFormatter("Parameters", toDescriptionList),
"@return" -> TagFormatter("Returns", toMarkdownList),
"@throws" -> TagFormatter("Throws", toDescriptionList),
"@see" -> TagFormatter("See Also", toMarkdownList),
"@example" -> TagFormatter("Examples", toCodeFences("scala")),
"@usecase" -> TagFormatter("Usecases", toCodeFences("scala")),
"@note" -> TagFormatter("Note", toMarkdownList),
"@author" -> TagFormatter("Authors", toMarkdownList),
"@since" -> TagFormatter("Since", toMarkdownList),
"@version" -> TagFormatter("Version", toMarkdownList)
)

/**
* Formats a list of items into a list describing them.
*
* Each element is assumed to consist of a first word, which is the item being described. The rest
* is the description of the item.
*
* @param items The items to format into a list.
* @return A markdown list of descriptions.
*/
private def toDescriptionList(ctx: Context, items: List[String]): String = {
val formattedItems = items.map { p =>
val name :: rest = p.split(" ", 2).toList
s"${bold(name)(ctx)} ${rest.mkString("").trim}"
}
toMarkdownList(ctx, formattedItems)
}

/**
* Formats a list of items into a markdown list.
*
* @param items The items to put in a list.
* @return The list of items, in markdown.
*/
private def toMarkdownList(ctx: Context, items: List[String]): String = {
val formattedItems = items.map(_.lines.mkString(System.lineSeparator + " "))
formattedItems.mkString(" - ", System.lineSeparator + " - ", "")
}

/**
* If the color is enabled, add syntax highlighting to each of `snippets`, otherwise wrap each
* of them in a markdown code fence.
* The results are put into a markdown list.
*
* @param language The language to use for the code fences
* @param snippets The list of snippets to format.
* @return A markdown list of code fences.
* @see toCodeFence
*/
private def toCodeFences(language: String)(ctx: Context, snippets: List[String]): String =
toMarkdownList(ctx, snippets.map(toCodeFence(language)(ctx, _)))

/**
* Formats `snippet` for display. If the color is enabled, the syntax is highlighted,
* otherwise the snippet is wrapped in a markdown code fence.
*
* @param language The language to use.
* @param snippet The code snippet
* @return `snippet`, wrapped in a code fence.
*/
private def toCodeFence(language: String)(ctx: Context, snippet: String): String = {
if (colorEnabled(ctx)) {
SyntaxHighlighting.highlight(snippet)(ctx)
} else {
s"""```$language
|$snippet
|```""".stripMargin
}
}

/**
* Format the elements of documentation associated with a given tag using `fn`, and starts the
* section with `title`.
*
* @param title The title to give to the formatted items.
* @param fn The formatting function to use.
*/
private case class TagFormatter(title: String, fn: (Context, List[String]) => String) {

/**
* Format `item` using `fn` if `items` is not empty.
*
* @param items The items to format
* @return If items is not empty, the items formatted using `fn`.
*/
def apply(items: List[String])(implicit ctx: Context): Option[String] = items match {
case Nil =>
None
case items =>
Some(s"""${bold(title)}
|${fn(ctx, items)}
|""".stripMargin)
}
}

/** Is the color enabled in the context? */
private def colorEnabled(implicit ctx: Context): Boolean =
ctx.settings.color.value != "never"

/** Show `str` in bold */
private def bold(str: String)(implicit ctx: Context): String = {
if (colorEnabled) s"$BOLD$str$RESET"
else s"**$str**"
}

}
8 changes: 3 additions & 5 deletions compiler/src/dotty/tools/repl/ReplCompiler.scala
Expand Up @@ -15,7 +15,7 @@ import dotty.tools.dotc.reporting.diagnostic.messages
import dotty.tools.dotc.transform.PostTyper
import dotty.tools.dotc.typer.ImportInfo
import dotty.tools.dotc.util.Positions._
import dotty.tools.dotc.util.SourceFile
import dotty.tools.dotc.util.{ParsedComment, SourceFile}
import dotty.tools.dotc.{CompilationUnit, Compiler, Run}
import dotty.tools.repl.results._

Expand Down Expand Up @@ -196,10 +196,8 @@ class ReplCompiler extends Compiler {
val symbols = extractSymbols(stat)
val doc = for {
sym <- symbols
docCtx <- ctx.docCtx
comment <- docCtx.docstring(sym)
body <- comment.expandedBody
} yield body
comment <- ParsedComment.docOf(sym)
} yield comment.renderAsMarkdown

if (doc.hasNext) doc.next()
else s"// No doc for `$expr`"
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/repl/ReplDriver.scala
Expand Up @@ -351,7 +351,7 @@ class ReplDriver(settings: Array[String],
case DocOf(expr) =>
compiler.docOf(expr)(newRun(state)).fold(
displayErrors,
res => out.println(SyntaxHighlighting.highlight(res)(state.context))
res => out.println(res)
)
state

Expand Down

0 comments on commit e27b6fa

Please sign in to comment.