Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5394 from dotty-staging/topic/ide-better-doc
IDE: Improve display of the documentation on hover
- Loading branch information
Showing
9 changed files
with
369 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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**" | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.