Skip to content

Commit e27b6fa

Browse files
authored
Merge pull request #5394 from dotty-staging/topic/ide-better-doc
IDE: Improve display of the documentation on hover
2 parents 6a315ff + cc1b7fa commit e27b6fa

File tree

9 files changed

+369
-59
lines changed

9 files changed

+369
-59
lines changed

compiler/src/dotty/tools/dotc/util/CommentParsing.scala

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
*/
66
package dotty.tools.dotc.util
77

8+
import scala.collection.mutable
9+
810
/** The comment parsing in `dotc` is used by both the comment cooking and the
911
* dottydoc tool.
1012
*
@@ -223,6 +225,15 @@ object CommentParsing {
223225
result
224226
}
225227

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

227238
def removeSections(raw: String, xs: String*): String = {
228239
val sections = tagIndex(raw)
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
package dotty.tools.dotc.util
2+
3+
import dotty.tools.dotc.core.Comments.{Comment, CommentsContext}
4+
import dotty.tools.dotc.core.Contexts.Context
5+
import dotty.tools.dotc.core.Names.TermName
6+
import dotty.tools.dotc.core.Symbols._
7+
import dotty.tools.dotc.printing.SyntaxHighlighting
8+
9+
import scala.Console.{BOLD, RESET, UNDERLINED}
10+
import scala.collection.immutable.ListMap
11+
import scala.util.matching.Regex
12+
13+
/**
14+
* A parsed doc comment.
15+
*
16+
* @param comment The doc comment to parse
17+
*/
18+
class ParsedComment(val comment: Comment) {
19+
20+
/**
21+
* The bounds of a section that represents the [start; end[ char offset
22+
* of the section within this comment's `content`.
23+
*/
24+
private type Bounds = (Int, Int)
25+
26+
/** The content of this comment, after expansion if possible. */
27+
val content: String = comment.expandedBody.getOrElse(comment.raw)
28+
29+
/** An index that marks all sections boundaries */
30+
private lazy val tagIndex: List[Bounds] = CommentParsing.tagIndex(content)
31+
32+
/**
33+
* Maps a parameter name to the bounds of its doc
34+
*
35+
* @see paramDoc
36+
*/
37+
private lazy val paramDocs: Map[String, Bounds] = CommentParsing.paramDocs(content, "@param", tagIndex)
38+
39+
/**
40+
* The "main" documentation for this comment. That is, the comment before any section starts.
41+
*/
42+
lazy val mainDoc: String = {
43+
val doc = tagIndex match {
44+
case Nil => content.stripSuffix("*/")
45+
case (start, _) :: _ => content.slice(0, start)
46+
}
47+
clean(doc.stripPrefix("/**"))
48+
}
49+
50+
/**
51+
* Renders this comment as markdown.
52+
*
53+
* The different sections are formatted according to the mapping in `knownTags`.
54+
*/
55+
def renderAsMarkdown(implicit ctx: Context): String = {
56+
val buf = new StringBuilder
57+
buf.append(mainDoc + System.lineSeparator + System.lineSeparator)
58+
val groupedSections = CommentParsing.groupedSections(content, tagIndex)
59+
60+
for {
61+
(tag, formatter) <- ParsedComment.knownTags
62+
boundss <- groupedSections.get(tag)
63+
texts = boundss.map { case (start, end) => clean(content.slice(start, end)) }
64+
formatted <- formatter(texts)
65+
} {
66+
buf.append(formatted)
67+
buf.append(System.lineSeparator)
68+
}
69+
70+
buf.toString
71+
}
72+
73+
/**
74+
* The `@param` section corresponding to `name`.
75+
*
76+
* @param name The parameter name whose documentation to extract.
77+
* @return The formatted documentation corresponding to `name`.
78+
*/
79+
def paramDoc(name: TermName): Option[String] = paramDocs.get(name.toString).map { case (start, end) =>
80+
val rawContent = content.slice(start, end)
81+
val docContent = ParsedComment.prefixRegex.replaceFirstIn(rawContent, "")
82+
clean(docContent)
83+
}
84+
85+
/**
86+
* Cleans `str`: remove prefixing `*` and trim the string.
87+
*
88+
* @param str The string to clean
89+
* @return The cleaned string.
90+
*/
91+
private def clean(str: String): String = str.stripMargin('*').trim
92+
}
93+
94+
object ParsedComment {
95+
96+
/**
97+
* Return the `ParsedComment` associated with `symbol`, if it exists.
98+
*
99+
* @param symbol The symbol for which to retrieve the documentation
100+
* @return If it exists, the `ParsedComment` for `symbol`.
101+
*/
102+
def docOf(symbol: Symbol)(implicit ctx: Context): Option[ParsedComment] = {
103+
val documentedSymbol = if (symbol.isPrimaryConstructor) symbol.owner else symbol
104+
for { docCtx <- ctx.docCtx
105+
comment <- docCtx.docstring(documentedSymbol)
106+
} yield new ParsedComment(comment)
107+
}
108+
109+
@scala.annotation.internal.sharable
110+
private val prefixRegex = """@param\s+\w+\s+""".r
111+
112+
/** A mapping from tag name to `TagFormatter` */
113+
private val knownTags: ListMap[String, TagFormatter] = ListMap(
114+
"@tparam" -> TagFormatter("Type Parameters", toDescriptionList),
115+
"@param" -> TagFormatter("Parameters", toDescriptionList),
116+
"@return" -> TagFormatter("Returns", toMarkdownList),
117+
"@throws" -> TagFormatter("Throws", toDescriptionList),
118+
"@see" -> TagFormatter("See Also", toMarkdownList),
119+
"@example" -> TagFormatter("Examples", toCodeFences("scala")),
120+
"@usecase" -> TagFormatter("Usecases", toCodeFences("scala")),
121+
"@note" -> TagFormatter("Note", toMarkdownList),
122+
"@author" -> TagFormatter("Authors", toMarkdownList),
123+
"@since" -> TagFormatter("Since", toMarkdownList),
124+
"@version" -> TagFormatter("Version", toMarkdownList)
125+
)
126+
127+
/**
128+
* Formats a list of items into a list describing them.
129+
*
130+
* Each element is assumed to consist of a first word, which is the item being described. The rest
131+
* is the description of the item.
132+
*
133+
* @param items The items to format into a list.
134+
* @return A markdown list of descriptions.
135+
*/
136+
private def toDescriptionList(ctx: Context, items: List[String]): String = {
137+
val formattedItems = items.map { p =>
138+
val name :: rest = p.split(" ", 2).toList
139+
s"${bold(name)(ctx)} ${rest.mkString("").trim}"
140+
}
141+
toMarkdownList(ctx, formattedItems)
142+
}
143+
144+
/**
145+
* Formats a list of items into a markdown list.
146+
*
147+
* @param items The items to put in a list.
148+
* @return The list of items, in markdown.
149+
*/
150+
private def toMarkdownList(ctx: Context, items: List[String]): String = {
151+
val formattedItems = items.map(_.lines.mkString(System.lineSeparator + " "))
152+
formattedItems.mkString(" - ", System.lineSeparator + " - ", "")
153+
}
154+
155+
/**
156+
* If the color is enabled, add syntax highlighting to each of `snippets`, otherwise wrap each
157+
* of them in a markdown code fence.
158+
* The results are put into a markdown list.
159+
*
160+
* @param language The language to use for the code fences
161+
* @param snippets The list of snippets to format.
162+
* @return A markdown list of code fences.
163+
* @see toCodeFence
164+
*/
165+
private def toCodeFences(language: String)(ctx: Context, snippets: List[String]): String =
166+
toMarkdownList(ctx, snippets.map(toCodeFence(language)(ctx, _)))
167+
168+
/**
169+
* Formats `snippet` for display. If the color is enabled, the syntax is highlighted,
170+
* otherwise the snippet is wrapped in a markdown code fence.
171+
*
172+
* @param language The language to use.
173+
* @param snippet The code snippet
174+
* @return `snippet`, wrapped in a code fence.
175+
*/
176+
private def toCodeFence(language: String)(ctx: Context, snippet: String): String = {
177+
if (colorEnabled(ctx)) {
178+
SyntaxHighlighting.highlight(snippet)(ctx)
179+
} else {
180+
s"""```$language
181+
|$snippet
182+
|```""".stripMargin
183+
}
184+
}
185+
186+
/**
187+
* Format the elements of documentation associated with a given tag using `fn`, and starts the
188+
* section with `title`.
189+
*
190+
* @param title The title to give to the formatted items.
191+
* @param fn The formatting function to use.
192+
*/
193+
private case class TagFormatter(title: String, fn: (Context, List[String]) => String) {
194+
195+
/**
196+
* Format `item` using `fn` if `items` is not empty.
197+
*
198+
* @param items The items to format
199+
* @return If items is not empty, the items formatted using `fn`.
200+
*/
201+
def apply(items: List[String])(implicit ctx: Context): Option[String] = items match {
202+
case Nil =>
203+
None
204+
case items =>
205+
Some(s"""${bold(title)}
206+
|${fn(ctx, items)}
207+
|""".stripMargin)
208+
}
209+
}
210+
211+
/** Is the color enabled in the context? */
212+
private def colorEnabled(implicit ctx: Context): Boolean =
213+
ctx.settings.color.value != "never"
214+
215+
/** Show `str` in bold */
216+
private def bold(str: String)(implicit ctx: Context): String = {
217+
if (colorEnabled) s"$BOLD$str$RESET"
218+
else s"**$str**"
219+
}
220+
221+
}

compiler/src/dotty/tools/repl/ReplCompiler.scala

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import dotty.tools.dotc.reporting.diagnostic.messages
1515
import dotty.tools.dotc.transform.PostTyper
1616
import dotty.tools.dotc.typer.ImportInfo
1717
import dotty.tools.dotc.util.Positions._
18-
import dotty.tools.dotc.util.SourceFile
18+
import dotty.tools.dotc.util.{ParsedComment, SourceFile}
1919
import dotty.tools.dotc.{CompilationUnit, Compiler, Run}
2020
import dotty.tools.repl.results._
2121

@@ -196,10 +196,8 @@ class ReplCompiler extends Compiler {
196196
val symbols = extractSymbols(stat)
197197
val doc = for {
198198
sym <- symbols
199-
docCtx <- ctx.docCtx
200-
comment <- docCtx.docstring(sym)
201-
body <- comment.expandedBody
202-
} yield body
199+
comment <- ParsedComment.docOf(sym)
200+
} yield comment.renderAsMarkdown
203201

204202
if (doc.hasNext) doc.next()
205203
else s"// No doc for `$expr`"

compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,7 @@ class ReplDriver(settings: Array[String],
351351
case DocOf(expr) =>
352352
compiler.docOf(expr)(newRun(state)).fold(
353353
displayErrors,
354-
res => out.println(SyntaxHighlighting.highlight(res)(state.context))
354+
res => out.println(res)
355355
)
356356
state
357357

0 commit comments

Comments
 (0)