Skip to content

Commit bb2c2c1

Browse files
authored
Show documentation on hover (#1025)
Closes #1006, blocked by #930.
1 parent bd6a6c4 commit bb2c2c1

File tree

4 files changed

+96
-24
lines changed

4 files changed

+96
-24
lines changed

effekt/jvm/src/test/scala/effekt/LSPTests.scala

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,37 @@ class LSPTests extends FunSuite {
276276
}
277277
}
278278

279+
test("Hovering over documented symbol shows doc comment") {
280+
withClientAndServer { (client, server) =>
281+
val (textDoc, cursor) = raw"""
282+
|/// Calculate the answer to the ultimate question of life, the universe, and everything
283+
|def calculate() = 42
284+
|
285+
|def main() = println(calculate())
286+
| ↑
287+
|""".textDocumentAndPosition
288+
val hoverContents =
289+
raw"""#### Function
290+
|```effekt
291+
|def calculate(): Int / {}
292+
|```
293+
| Calculate the answer to the ultimate question of life, the universe, and everything
294+
|""".stripMargin
295+
296+
val didOpenParams = new DidOpenTextDocumentParams()
297+
didOpenParams.setTextDocument(textDoc)
298+
server.getTextDocumentService().didOpen(didOpenParams)
299+
300+
val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, cursor)
301+
val hover = server.getTextDocumentService().hover(hoverParams).get()
302+
303+
val expectedHover = new Hover()
304+
expectedHover.setRange(new Range(cursor, cursor))
305+
expectedHover.setContents(new MarkupContent("markdown", hoverContents))
306+
assertEquals(hover, expectedHover)
307+
}
308+
}
309+
279310
test("Hovering over hole shows inside and outside types") {
280311
withClientAndServer { (client, server) =>
281312
val (textDoc, cursor) = raw"""

effekt/shared/src/main/scala/effekt/Intelligence.scala

Lines changed: 54 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package effekt
22

33
import effekt.context.{Annotations, Context}
4-
import effekt.source.{FunDef, Include, Maybe, ModuleDecl, Span, Tree}
4+
import effekt.source.{FunDef, Include, Maybe, ModuleDecl, Span, Tree, Doc}
55
import effekt.symbols.{CaptureSet, Hole}
66
import kiama.util.{Position, Source}
77
import effekt.symbols.scopes.Scope
@@ -18,27 +18,35 @@ trait Intelligence {
1818
symbol: Symbol,
1919
header: String,
2020
signature: Option[String],
21-
description: Option[String]
21+
description: Option[String],
22+
documentation: Doc
2223
) {
2324
def fullDescription: String = {
2425
val sig = signature.map(sig => s"```effekt\n$sig\n```").getOrElse { "" }
2526
val desc = description.getOrElse("")
27+
val doc = showDocumentation(documentation)
2628

2729
s"""|#### $header
2830
|$sig
29-
|$desc
31+
|$desc$doc
3032
|""".stripMargin
3133
}
3234

3335
def shortDescription: String = {
3436
val sig = signature.map(sig => s"```effekt\n$sig\n```").getOrElse { "" }
37+
val doc = showDocumentation(documentation)
3538

3639
s"""|#### $header
37-
|$sig
40+
|$sig$doc
3841
|""".stripMargin
3942
}
4043
}
4144

45+
def showDocumentation(documentation: Doc): String =
46+
documentation.map('\n' +: _)
47+
.getOrElse("")
48+
.replace("\\n", "\n")
49+
4250
private def sortByPosition(trees: Vector[Tree])(using C: Context): Vector[Tree] =
4351
val pos = C.positions
4452
trees.sortWith {
@@ -110,6 +118,16 @@ trait Intelligence {
110118
case u => C.definitionTreeOption(u)
111119
}
112120

121+
def getDocumentationOf(s: Symbol)(using C: Context): Doc =
122+
getDefinitionOf(s).asInstanceOf[Option[Any]] match {
123+
case Some(p: Product) =>
124+
p.productElementNames.zip(p.productIterator)
125+
.collectFirst {
126+
case ("doc", Some(s: String)) => s
127+
}
128+
case _ => None
129+
}
130+
113131
// For now, only show the first call target
114132
def resolveCallTarget(sym: Symbol): Symbol = sym match {
115133
case t: CallTarget => t.symbols.flatten.headOption.getOrElse(sym)
@@ -204,12 +222,15 @@ trait Intelligence {
204222
def getInfoOf(sym: Symbol)(using C: Context): Option[SymbolInfo] = PartialFunction.condOpt(resolveCallTarget(sym)) {
205223

206224
case b: ExternFunction =>
207-
SymbolInfo(b, "External function definition", Some(DeclPrinter(b)), None)
225+
val doc = getDocumentationOf(b)
226+
SymbolInfo(b, "External function definition", Some(DeclPrinter(b)), None, doc)
208227

209228
case f: UserFunction if C.functionTypeOption(f).isDefined =>
210-
SymbolInfo(f, "Function", Some(DeclPrinter(f)), None)
229+
val doc = getDocumentationOf(f)
230+
SymbolInfo(f, "Function", Some(DeclPrinter(f)), None, doc)
211231

212232
case f: Operation =>
233+
val doc = getDocumentationOf(f)
213234
val ex =
214235
pp"""|Effect operations, like `${f.name}` allow to express non-local control flow.
215236
|
@@ -229,32 +250,39 @@ trait Intelligence {
229250
|handled by the handler. This is important when considering higher-order functions.
230251
|""".stripMargin
231252

232-
SymbolInfo(f, "Effect operation", Some(DeclPrinter(f)), Some(ex))
253+
SymbolInfo(f, "Effect operation", Some(DeclPrinter(f)), Some(ex), doc)
233254

234255
case f: EffectAlias =>
235-
SymbolInfo(f, "Effect alias", Some(DeclPrinter(f)), None)
256+
val doc = getDocumentationOf(f)
257+
SymbolInfo(f, "Effect alias", Some(DeclPrinter(f)), None, doc)
236258

237259
case t: TypeAlias =>
238-
SymbolInfo(t, "Type alias", Some(DeclPrinter(t)), None)
260+
val doc = getDocumentationOf(t)
261+
SymbolInfo(t, "Type alias", Some(DeclPrinter(t)), None, doc)
239262

240263
case t: ExternType =>
241-
SymbolInfo(t, "External type definition", Some(DeclPrinter(t)), None)
264+
val doc = getDocumentationOf(t)
265+
SymbolInfo(t, "External type definition", Some(DeclPrinter(t)), None, doc)
242266

243267
case t: ExternInterface =>
244-
SymbolInfo(t, "External interface definition", Some(DeclPrinter(t)), None)
268+
val doc = getDocumentationOf(t)
269+
SymbolInfo(t, "External interface definition", Some(DeclPrinter(t)), None, doc)
245270

246271
case t: ExternResource =>
247-
SymbolInfo(t, "External resource definition", Some(DeclPrinter(t)), None)
272+
val doc = getDocumentationOf(t)
273+
SymbolInfo(t, "External resource definition", Some(DeclPrinter(t)), None, doc)
248274

249275
case c: Constructor =>
276+
val doc = getDocumentationOf(c)
250277
val ex = pp"""|Instances of data types like `${c.tpe}` can only store
251278
|_values_, not _blocks_. Hence, constructors like `${c.name}` only have
252279
|value parameter lists, not block parameters.
253280
|""".stripMargin
254281

255-
SymbolInfo(c, s"Constructor of data type `${c.tpe}`", Some(DeclPrinter(c)), Some(ex))
282+
SymbolInfo(c, s"Constructor of data type `${c.tpe}`", Some(DeclPrinter(c)), Some(ex), doc)
256283

257284
case c: BlockParam =>
285+
val doc = getDocumentationOf(c)
258286
val signature = C.functionTypeOption(c).map { tpe => pp"{ ${c.name}: ${tpe} }" }
259287

260288
val ex =
@@ -267,9 +295,10 @@ trait Intelligence {
267295
|yielded to.
268296
|""".stripMargin
269297

270-
SymbolInfo(c, "Block parameter", signature, Some(ex))
298+
SymbolInfo(c, "Block parameter", signature, Some(ex), doc)
271299

272300
case c: ResumeParam =>
301+
val doc = getDocumentationOf(c)
273302
val tpe = C.functionTypeOption(c)
274303
val signature = tpe.map { tpe => pp"{ ${c.name}: ${tpe} }" }
275304
val hint = tpe.map { tpe => pp"(i.e., `${tpe.result}`)" }.getOrElse { " " }
@@ -284,9 +313,10 @@ trait Intelligence {
284313
|- the return type of the resumption.
285314
|""".stripMargin
286315

287-
SymbolInfo(c, "Resumption", signature, Some(ex))
316+
SymbolInfo(c, "Resumption", signature, Some(ex), doc)
288317

289318
case c: VarBinder =>
319+
val doc = getDocumentationOf(c)
290320
val signature = C.blockTypeOption(c).map(TState.extractType).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" }
291321

292322
val ex =
@@ -299,9 +329,10 @@ trait Intelligence {
299329
|combination with effect handlers.
300330
""".stripMargin
301331

302-
SymbolInfo(c, "Mutable variable binder", signature, Some(ex))
332+
SymbolInfo(c, "Mutable variable binder", signature, Some(ex), doc)
303333

304334
case s: RegBinder =>
335+
val doc = getDocumentationOf(s)
305336
val signature = C.blockTypeOption(s).map(TState.extractType).orElse(s.tpe).map { tpe => pp"${s.name}: ${tpe}" }
306337

307338
val ex =
@@ -310,19 +341,22 @@ trait Intelligence {
310341
|in combination with continuation capture and resumption.
311342
|""".stripMargin
312343

313-
SymbolInfo(s, "Variable in region", signature, Some(ex))
344+
SymbolInfo(s, "Variable in region", signature, Some(ex), doc)
314345

315346
case c: ValueParam =>
347+
val doc = getDocumentationOf(c)
316348
val signature = C.valueTypeOption(c).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" }
317-
SymbolInfo(c, "Value parameter", signature, None)
349+
SymbolInfo(c, "Value parameter", signature, None, doc)
318350

319351
case c: ValBinder =>
352+
val doc = getDocumentationOf(c)
320353
val signature = C.valueTypeOption(c).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" }
321-
SymbolInfo(c, "Value binder", signature, None)
354+
SymbolInfo(c, "Value binder", signature, None, doc)
322355

323356
case c: DefBinder =>
357+
val doc = getDocumentationOf(c)
324358
val signature = C.blockTypeOption(c).orElse(c.tpe).map { tpe => pp"${c.name}: ${tpe}" }
325-
SymbolInfo(c, "Block binder", signature, None)
359+
SymbolInfo(c, "Block binder", signature, None, doc)
326360
}
327361
}
328362

effekt/shared/src/main/scala/effekt/RecursiveDescent.scala

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -311,13 +311,19 @@ class RecursiveDescent(positions: Positions, tokens: Seq[Token], source: Source)
311311
nonterminal:
312312
// skip spaces at the start
313313
spaces()
314-
val doc = maybeDocumentation()
315-
val res = ModuleDecl(moduleDecl(), manyWhile(includeDecl(), `import`), toplevelDefs(), doc, span())
314+
val (name, doc) = moduleDecl()
315+
val res = ModuleDecl(name, manyWhile(includeDecl(), `import`), toplevelDefs(), doc, span())
316316
if peek(`EOF`) then res else fail("Unexpected end of input")
317317
// failure("Required at least one top-level function or effect definition")
318318

319-
def moduleDecl(): String =
320-
when(`module`) { moduleName() } { defaultModulePath }
319+
def moduleDecl(): Tuple2[String, Doc] =
320+
documentedKind match {
321+
case `module` =>
322+
val doc = maybeDocumentation()
323+
consume(`module`)
324+
(moduleName(), doc)
325+
case _ => (defaultModulePath, None)
326+
}
321327

322328
// we are purposefully not using File here since the parser needs to work both
323329
// on the JVM and in JavaScript

examples/benchmarks/other/nbe.effekt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
/// Demo of an effectful Normalization by Evaluation (NbE) implementation for the pure, untyped lambda calculus.
22
/// Uses the Call-by-Value reduction order, same as host.
3+
module examples/benchmarks/nbe
34

45
import io
56
import map

0 commit comments

Comments
 (0)