Skip to content

Commit a9827f5

Browse files
authored
Inlay hints: scaffolding and captures (#828)
Requires the newest version of the VSCode extension (v0.2.4) Requires that the VSCode user enables inlay hints (see a separate comment below on how to set them up). As part of the scaffolding, I also replaced Scala 2 `implicit`s with Scala 3 `using` Instead of using our hand-rolled `inferredCaptures` command, we can _just_ use inlay hints 🥳 (See #524 for more details) Screenshots: <img width="651" alt="Screenshot 2025-02-15 at 10 49 38" src="https://github.com/user-attachments/assets/bb00d842-70a8-4d83-83e9-b436e11a59b4" /> <img width="576" alt="Screenshot 2025-02-15 at 10 54 19" src="https://github.com/user-attachments/assets/fe1ce6a7-ec6b-4034-b718-c2ab8d36ca60" />
1 parent a445cc5 commit a9827f5

File tree

3 files changed

+56
-44
lines changed

3 files changed

+56
-44
lines changed

effekt/js/src/main/scala/effekt/LanguageServer.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ class LanguageServer extends Intelligence {
122122
@JSExport
123123
def inferredCaptures(path: String): js.Array[lsp.CaptureInfo] = {
124124
typecheck(path)
125-
getInferredCaptures(VirtualFileSource(path)).map {
125+
val source = VirtualFileSource(path)
126+
val range = kiama.util.Range(source.offsetToPosition(0), source.offsetToPosition(source.charCount))
127+
getInferredCaptures(range).map {
126128
case (p, c) => new lsp.CaptureInfo(toLSPPosition(p), c.toString)
127129
}.toJSArray
128130
}

effekt/jvm/src/main/scala/effekt/Server.scala

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import effekt.core.PrettyPrinter
55
import effekt.source.{ FunDef, Hole, ModuleDecl, Tree }
66
import effekt.util.{ PlainMessaging, getOrElseAborting }
77
import effekt.util.messages.EffektError
8-
import kiama.util.{ Filenames, Notebook, NotebookCell, Position, Services, Source }
8+
import kiama.util.{ Filenames, Notebook, NotebookCell, Position, Services, Source, Range }
99
import kiama.output.PrettyPrinterTypes.Document
1010
import org.eclipse.lsp4j.{ Diagnostic, DocumentSymbol, ExecuteCommandParams, SymbolKind }
1111

@@ -30,7 +30,7 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
3030
diagnostic(message.range, lspMessaging.formatContent(message), message.severity)
3131

3232
override def getDefinition(position: Position): Option[Tree] =
33-
getDefinitionAt(position)(context)
33+
getDefinitionAt(position)(using context)
3434

3535
/**
3636
* Overriding hook to also publish core and target for LSP server
@@ -83,14 +83,14 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
8383
getSymbolHover(position) orElse getHoleHover(position)
8484

8585
def getSymbolHover(position: Position): Option[String] = for {
86-
(tree, sym) <- getSymbolAt(position)(context)
87-
info <- getInfoOf(sym)(context)
86+
(tree, sym) <- getSymbolAt(position)(using context)
87+
info <- getInfoOf(sym)(using context)
8888
} yield if (settingBool("showExplanations")) info.fullDescription else info.shortDescription
8989

9090
def getHoleHover(position: Position): Option[String] = for {
91-
trees <- getTreesAt(position)(context)
91+
trees <- getTreesAt(position)(using context)
9292
tree <- trees.collectFirst { case h: source.Hole => h }
93-
info <- getHoleInfo(tree)(context)
93+
info <- getHoleInfo(tree)(using context)
9494
} yield info
9595

9696
def positionToLocation(p: Position): Location = {
@@ -115,17 +115,26 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
115115
id <- context.definitionTreeOption(sym)
116116
decl <- getSourceTreeFor(sym)
117117
kind <- getSymbolKind(sym)
118-
detail <- getInfoOf(sym)(context)
118+
detail <- getInfoOf(sym)(using context)
119119
} yield new DocumentSymbol(sym.name.name, kind, rangeOfNode(decl), rangeOfNode(id), detail.header)
120120
Some(documentSymbols)
121121

122122
override def getReferences(position: Position, includeDecl: Boolean): Option[Vector[Tree]] =
123123
for {
124-
(tree, sym) <- getSymbolAt(position)(context)
124+
(tree, sym) <- getSymbolAt(position)(using context)
125125
refs = context.distinctReferencesTo(sym)
126126
allRefs = if (includeDecl) tree :: refs else refs
127127
} yield allRefs.toVector
128128

129+
override def getInlayHints(range: kiama.util.Range): Option[Vector[InlayHint]] =
130+
val captures = getInferredCaptures(range)(using context).map {
131+
case (p, c) =>
132+
val prettyCaptures = TypePrinter.show(c)
133+
InlayHint(InlayHintKind.Type, p, prettyCaptures, markdownTooltip = s"captures: `${prettyCaptures}`", paddingRight = true, effektKind = "capture")
134+
}.toVector
135+
136+
if captures.isEmpty then None else Some(captures)
137+
129138
// settings might be null
130139
override def setSettings(settings: Object): Unit = {
131140
import com.google.gson.JsonObject
@@ -153,11 +162,11 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
153162

154163
override def getCodeActions(position: Position): Option[Vector[TreeAction]] =
155164
Some(for {
156-
trees <- getTreesAt(position)(context).toVector
157-
actions <- trees.flatMap { t => action(t)(context) }
165+
trees <- getTreesAt(position)(using context).toVector
166+
actions <- trees.flatMap { t => action(t)(using context) }
158167
} yield actions)
159168

160-
def action(tree: Tree)(implicit C: Context): Option[TreeAction] = tree match {
169+
def action(tree: Tree)(using C: Context): Option[TreeAction] = tree match {
161170
case f: FunDef => inferEffectsAction(f)
162171
case h: Hole => closeHoleAction(h)
163172
case _ => None
@@ -213,18 +222,6 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
213222
tpe1 != tpe2 || effs1 != effs2
214223
}
215224

216-
case class CaptureInfo(location: Location, captureText: String)
217-
218-
override def executeCommand(src: Source, params: ExecuteCommandParams): Option[Any] =
219-
if (params.getCommand == "inferredCaptures") {
220-
val captures = getInferredCaptures(src)(using context).map {
221-
case (p, c) => CaptureInfo(positionToLocation(p), TypePrinter.show(c))
222-
}
223-
if (captures.isEmpty) None else Some(captures.toArray)
224-
} else {
225-
None
226-
}
227-
228225
override def createServices(config: EffektConfig) = new LSPServices(this, config)
229226

230227
// Class to easily test custom LSP services not (yet) meant to go into kiama.Services

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

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,9 @@ trait Intelligence {
3636
}
3737
}
3838

39-
def getTreesAt(position: Position)(implicit C: Context): Option[Vector[Tree]] = for {
40-
decl <- C.compiler.getAST(position.source)
41-
tree = new EffektTree(decl)
42-
allTrees = tree.nodes.collect { case t: Tree => t }
43-
pos = C.positions
44-
trees = pos.findNodesContaining(allTrees, position)
45-
nodes = trees.sortWith {
39+
private def sortByPosition(trees: Vector[Tree])(using C: Context): Vector[Tree] =
40+
val pos = C.positions
41+
trees.sortWith {
4642
(t1, t2) =>
4743
val p1s = pos.getStart(t1).get
4844
val p2s = pos.getStart(t2).get
@@ -55,14 +51,29 @@ trait Intelligence {
5551
p2s < p1s
5652
}
5753
}
54+
55+
def getTreesAt(position: Position)(using C: Context): Option[Vector[Tree]] = for {
56+
decl <- C.compiler.getAST(position.source)
57+
tree = new EffektTree(decl)
58+
allTrees = tree.nodes.collect { case t: Tree => t }
59+
trees = C.positions.findNodesContaining(allTrees, position)
60+
nodes = sortByPosition(trees)
61+
} yield nodes
62+
63+
def getTreesInRange(range: kiama.util.Range)(using C: Context): Option[Vector[Tree]] = for {
64+
decl <- C.compiler.getAST(range.from.source)
65+
tree = new EffektTree(decl)
66+
allTrees = tree.nodes.collect { case t: Tree => t }
67+
trees = C.positions.findNodesInRange(allTrees, range)
68+
nodes = sortByPosition(trees)
5869
} yield nodes
5970

60-
def getIdTreeAt(position: Position)(implicit C: Context): Option[source.Id] = for {
71+
def getIdTreeAt(position: Position)(using C: Context): Option[source.Id] = for {
6172
trees <- getTreesAt(position)
6273
id <- trees.collectFirst { case id: source.Id => id }
6374
} yield id
6475

65-
def getSymbolAt(position: Position)(implicit C: Context): Option[(Tree, Symbol)] =
76+
def getSymbolAt(position: Position)(using C: Context): Option[(Tree, Symbol)] =
6677
def identifiers = for {
6778
id <- getIdTreeAt(position)
6879
sym <- C.symbolOption(id)
@@ -82,12 +93,12 @@ trait Intelligence {
8293
case _ => None
8394
}
8495

85-
def getDefinitionAt(position: Position)(implicit C: Context): Option[Tree] = for {
96+
def getDefinitionAt(position: Position)(using C: Context): Option[Tree] = for {
8697
(_, sym) <- getSymbolAt(position)
8798
decl <- getDefinitionOf(resolveCallTarget(sym))
8899
} yield decl
89100

90-
def getDefinitionOf(s: Symbol)(implicit C: Context): Option[Tree] = s match {
101+
def getDefinitionOf(s: Symbol)(using C: Context): Option[Tree] = s match {
91102
case u: UserFunction => Some(u.decl)
92103
case u: Binder => Some(u.decl)
93104
case d: Operation => C.definitionTreeOption(d.interface)
@@ -102,9 +113,9 @@ trait Intelligence {
102113
case s => s
103114
}
104115

105-
def getHoleInfo(hole: source.Hole)(implicit C: Context): Option[String] = for {
106-
outerTpe <- C.inferredTypeAndEffectOption(hole)
107-
innerTpe <- C.inferredTypeAndEffectOption(hole.stmts)
116+
def getHoleInfo(hole: source.Hole)(using C: Context): Option[String] = for {
117+
(outerTpe, outerEff) <- C.inferredTypeAndEffectOption(hole)
118+
(innerTpe, innerEff) <- C.inferredTypeAndEffectOption(hole.stmts)
108119
} yield pp"""| | Outside | Inside |
109120
| |:------------- |:------------- |
110121
| | `${outerTpe}` | `${innerTpe}` |
@@ -113,12 +124,14 @@ trait Intelligence {
113124
def allCaptures(src: Source)(using C: Context): List[(Tree, CaptureSet)] =
114125
C.annotationOption(Annotations.CaptureForFile, src).getOrElse(Nil)
115126

116-
// For now we only show captures of function definitions and calls to box
117-
def getInferredCaptures(src: Source)(using C: Context): List[(Position, CaptureSet)] =
127+
// For now, we only show captures of function definitions and calls to box
128+
def getInferredCaptures(range: kiama.util.Range)(using C: Context): List[(Position, CaptureSet)] =
129+
val src = range.from.source
118130
allCaptures(src).filter {
119-
case (t, c) =>
120-
val p = C.positions.getStart(t)
121-
p.isDefined
131+
// keep only captures in the current range
132+
case (t, c) => C.positions.getStart(t) match
133+
case Some(p) => p.between(range.from, range.to)
134+
case None => false
122135
}.collect {
123136
case (t: source.FunDef, c) => for {
124137
pos <- C.positions.getStart(t)
@@ -132,7 +145,7 @@ trait Intelligence {
132145
} yield (pos, capt)
133146
}.flatten
134147

135-
def getInfoOf(sym: Symbol)(implicit C: Context): Option[SymbolInfo] = PartialFunction.condOpt(resolveCallTarget(sym)) {
148+
def getInfoOf(sym: Symbol)(using C: Context): Option[SymbolInfo] = PartialFunction.condOpt(resolveCallTarget(sym)) {
136149

137150
case b: ExternFunction =>
138151
SymbolInfo(b, "External function definition", Some(DeclPrinter(b)), None)

0 commit comments

Comments
 (0)