Skip to content

Commit c20fb75

Browse files
committed
Format documentation in REPL
1 parent 6deee31 commit c20fb75

File tree

5 files changed

+76
-48
lines changed

5 files changed

+76
-48
lines changed

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

Lines changed: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import dotty.tools.dotc.core.Comments.{Comment, CommentsContext}
44
import dotty.tools.dotc.core.Contexts.Context
55
import dotty.tools.dotc.core.Names.TermName
66
import dotty.tools.dotc.core.Symbols._
7+
import dotty.tools.dotc.printing.SyntaxHighlighting
78

9+
import scala.Console.{BOLD, RESET, UNDERLINED}
810
import scala.collection.immutable.ListMap
911
import scala.util.matching.Regex
1012

@@ -50,7 +52,7 @@ class ParsedComment(val comment: Comment) {
5052
*
5153
* The different sections are formatted according to the mapping in `knownTags`.
5254
*/
53-
def renderAsMarkdown: String = {
55+
def renderAsMarkdown(implicit ctx: Context): String = {
5456
val buf = new StringBuilder
5557
buf.append(mainDoc + System.lineSeparator + System.lineSeparator)
5658
val groupedSections = CommentParsing.groupedSections(content, tagIndex)
@@ -131,12 +133,12 @@ object ParsedComment {
131133
* @param items The items to format into a list.
132134
* @return A markdown list of descriptions.
133135
*/
134-
private def toDescriptionList(items: List[String]): String = {
136+
private def toDescriptionList(ctx: Context, items: List[String]): String = {
135137
val formattedItems = items.map { p =>
136138
val name :: rest = p.split(" ", 2).toList
137-
s"**$name** ${rest.mkString("").trim}"
139+
s"${bold(name)(ctx)} ${rest.mkString("").trim}"
138140
}
139-
toMarkdownList(formattedItems)
141+
toMarkdownList(ctx, formattedItems)
140142
}
141143

142144
/**
@@ -145,34 +147,41 @@ object ParsedComment {
145147
* @param items The items to put in a list.
146148
* @return The list of items, in markdown.
147149
*/
148-
private def toMarkdownList(items: List[String]): String = {
150+
private def toMarkdownList(ctx: Context, items: List[String]): String = {
149151
val formattedItems = items.map(_.lines.mkString(System.lineSeparator + " "))
150152
formattedItems.mkString(" - ", System.lineSeparator + " - ", "")
151153
}
152154

153155
/**
154-
* Wrap each of `snippets` into a markdown code fence, using `language`. All the code fences are
155-
* put into a markdown list.
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.
156159
*
157160
* @param language The language to use for the code fences
158161
* @param snippets The list of snippets to format.
159162
* @return A markdown list of code fences.
160163
* @see toCodeFence
161164
*/
162-
private def toCodeFences(language: String)(snippets: List[String]): String =
163-
toMarkdownList(snippets.map(toCodeFence(language)))
165+
private def toCodeFences(language: String)(ctx: Context, snippets: List[String]): String =
166+
toMarkdownList(ctx, snippets.map(toCodeFence(language)(ctx, _)))
164167

165168
/**
166-
* Wraps `snippet` in a markdown code fence, using `language`.
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.
167171
*
168172
* @param language The language to use.
169173
* @param snippet The code snippet
170174
* @return `snippet`, wrapped in a code fence.
171175
*/
172-
private def toCodeFence(language: String)(snippet: String): String =
173-
s"""```$language
174-
|$snippet
175-
|```""".stripMargin
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+
}
176185

177186
/**
178187
* Format the elements of documentation associated with a given tag using `fn`, and starts the
@@ -181,22 +190,41 @@ object ParsedComment {
181190
* @param title The title to give to the formatted items.
182191
* @param fn The formatting function to use.
183192
*/
184-
private case class TagFormatter(title: String, fn: List[String] => String) {
193+
private case class TagFormatter(title: String, fn: (Context, List[String]) => String) {
185194

186195
/**
187196
* Format `item` using `fn` if `items` is not empty.
188197
*
189198
* @param items The items to format
190199
* @return If items is not empty, the items formatted using `fn`.
191200
*/
192-
def apply(items: List[String]): Option[String] = items match {
201+
def apply(items: List[String])(implicit ctx: Context): Option[String] = items match {
193202
case Nil =>
194203
None
195204
case items =>
196-
Some(s"""#### $title:
197-
|${fn(items)}
205+
Some(s"""${heading(title)}:
206+
|${fn(ctx, items)}
198207
|""".stripMargin)
199208
}
200209
}
201210

211+
/** Is the color enabled in the context? */
212+
private def colorEnabled(implicit ctx: Context): Boolean =
213+
ctx.settings.color.value != "never"
214+
215+
/**
216+
* If the color is enabled, underline `str`, otherwise make it a markdown header by
217+
* prepending `####`.
218+
*/
219+
private def heading(str: String)(implicit ctx: Context): String = {
220+
if (colorEnabled) s"$UNDERLINED$str$RESET"
221+
else s"#### $str"
222+
}
223+
224+
/** Show `str` in bold */
225+
private def bold(str: String)(implicit ctx: Context): String = {
226+
if (colorEnabled) s"$BOLD$str$RESET"
227+
else s"**$str**"
228+
}
229+
202230
}

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

compiler/test/dotty/tools/repl/DocTests.scala

Lines changed: 23 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,92 +8,92 @@ class DocTests extends ReplTest {
88

99
@Test def docOfDef =
1010
eval("/** doc */ def foo = 0").andThen { implicit s =>
11-
assertEquals("/** doc */", doc("foo"))
11+
assertEquals("doc", doc("foo"))
1212
}
1313

1414
@Test def docOfVal =
1515
eval("/** doc */ val foo = 0").andThen { implicit s =>
16-
assertEquals("/** doc */", doc("foo"))
16+
assertEquals("doc", doc("foo"))
1717
}
1818

1919
@Test def docOfObject =
2020
eval("/** doc */ object Foo").andThen { implicit s =>
21-
assertEquals("/** doc */", doc("Foo"))
21+
assertEquals("doc", doc("Foo"))
2222
}
2323

2424
@Test def docOfClass =
2525
eval("/** doc */ class Foo").andThen { implicit s =>
26-
assertEquals("/** doc */", doc("new Foo"))
26+
assertEquals("doc", doc("new Foo"))
2727
}
2828

2929
@Test def docOfTrait =
3030
eval("/** doc */ trait Foo").andThen { implicit s =>
31-
assertEquals("/** doc */", doc("new Foo"))
31+
assertEquals("doc", doc("new Foo"))
3232
}
3333

3434
@Test def docOfDefInObject =
3535
eval("object O { /** doc */ def foo = 0 }").andThen { implicit s =>
36-
assertEquals("/** doc */", doc("O.foo"))
36+
assertEquals("doc", doc("O.foo"))
3737
}
3838

3939
@Test def docOfValInObject =
4040
eval("object O { /** doc */ val foo = 0 }").andThen { implicit s =>
41-
assertEquals("/** doc */", doc("O.foo"))
41+
assertEquals("doc", doc("O.foo"))
4242
}
4343

4444
@Test def docOfObjectInObject =
4545
eval("object O { /** doc */ object Foo }").andThen { implicit s =>
46-
assertEquals("/** doc */", doc("O.Foo"))
46+
assertEquals("doc", doc("O.Foo"))
4747
}
4848

4949
@Test def docOfClassInObject =
5050
eval("object O { /** doc */ class Foo }").andThen { implicit s =>
51-
assertEquals("/** doc */", doc("new O.Foo"))
51+
assertEquals("doc", doc("new O.Foo"))
5252
}
5353

5454
@Test def docOfTraitInObject =
5555
eval("object O { /** doc */ trait Foo }").andThen { implicit s =>
56-
assertEquals("/** doc */", doc("new O.Foo"))
56+
assertEquals("doc", doc("new O.Foo"))
5757
}
5858

5959
@Test def docOfDefInClass =
6060
eval(
6161
"""class C { /** doc */ def foo = 0 }
6262
|val c = new C
6363
""".stripMargin).andThen { implicit s =>
64-
assertEquals("/** doc */", doc("c.foo"))
64+
assertEquals("doc", doc("c.foo"))
6565
}
6666

6767
@Test def docOfValInClass =
6868
eval(
6969
"""class C { /** doc */ val foo = 0 }
7070
|val c = new C
7171
""".stripMargin).andThen { implicit s =>
72-
assertEquals("/** doc */", doc("c.foo"))
72+
assertEquals("doc", doc("c.foo"))
7373
}
7474

7575
@Test def docOfObjectInClass =
7676
eval(
7777
"""class C { /** doc */ object Foo }
7878
|val c = new C
7979
""".stripMargin).andThen { implicit s =>
80-
assertEquals("/** doc */", doc("c.Foo"))
80+
assertEquals("doc", doc("c.Foo"))
8181
}
8282

8383
@Test def docOfClassInClass =
8484
eval(
8585
"""class C { /** doc */ class Foo }
8686
|val c = new C
8787
""".stripMargin).andThen { implicit s =>
88-
assertEquals("/** doc */", doc("new c.Foo"))
88+
assertEquals("doc", doc("new c.Foo"))
8989
}
9090

9191
@Test def docOfTraitInClass =
9292
eval(
9393
"""class C { /** doc */ trait Foo }
9494
|val c = new C
9595
""".stripMargin).andThen { implicit s =>
96-
assertEquals("/** doc */", doc("new c.Foo"))
96+
assertEquals("doc", doc("new c.Foo"))
9797
}
9898

9999
@Test def docOfOverloadedDef =
@@ -103,16 +103,16 @@ class DocTests extends ReplTest {
103103
| /** doc1 */ def foo(x: String) = x
104104
|}
105105
""".stripMargin).andThen { implicit s =>
106-
assertEquals("/** doc0 */", doc("O.foo(_: Int)"))
107-
assertEquals("/** doc1 */", doc("O.foo(_: String)"))
106+
assertEquals("doc0", doc("O.foo(_: Int)"))
107+
assertEquals("doc1", doc("O.foo(_: String)"))
108108
}
109109

110110
@Test def docOfInherited =
111111
eval(
112112
"""class C { /** doc */ def foo = 0 }
113113
|object O extends C
114114
""".stripMargin).andThen { implicit s =>
115-
assertEquals("/** doc */", doc("O.foo"))
115+
assertEquals("doc", doc("O.foo"))
116116
}
117117

118118
@Test def docOfOverride =
@@ -126,8 +126,8 @@ class DocTests extends ReplTest {
126126
| /** overridden doc */ override def foo(x: String): String = x
127127
|}
128128
""".stripMargin).andThen { implicit s =>
129-
assertEquals("/** doc0 */", doc("O.foo(_: Int)"))
130-
assertEquals("/** overridden doc */", doc("O.foo(_: String)"))
129+
assertEquals("doc0", doc("O.foo(_: Int)"))
130+
assertEquals("overridden doc", doc("O.foo(_: String)"))
131131
}
132132

133133
@Test def docOfOverrideObject =
@@ -142,8 +142,8 @@ class DocTests extends ReplTest {
142142
| }
143143
|}
144144
""".stripMargin).andThen { implicit s =>
145-
assertEquals("/** companion */", doc("O.foo"))
146-
assertEquals("/** doc0 */", doc("O.foo.bar"))
145+
assertEquals("companion", doc("O.foo"))
146+
assertEquals("doc0", doc("O.foo.bar"))
147147
}
148148

149149
@Test def docIsCooked =
@@ -157,7 +157,7 @@ class DocTests extends ReplTest {
157157
| def hello = "world"
158158
|}
159159
""".stripMargin).andThen { implicit s =>
160-
assertEquals("/** Expansion: some-value */", doc("Foo.hello"))
160+
assertEquals("Expansion: some-value", doc("Foo.hello"))
161161
}
162162

163163
private def eval(code: String): State =

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -607,7 +607,9 @@ object DottyLanguageServer {
607607
}
608608
}
609609

610-
private def hoverContent(typeInfo: Option[String], comment: Option[ParsedComment]): lsp4j.MarkupContent = {
610+
private def hoverContent(typeInfo: Option[String],
611+
comment: Option[ParsedComment]
612+
)(implicit ctx: Context): lsp4j.MarkupContent = {
611613
val buf = new StringBuilder
612614
typeInfo.foreach { info =>
613615
buf.append(s"""```scala

0 commit comments

Comments
 (0)