Repository: effekt-lang/effekt (scala)

Issue:
Mandatory capture annotation in extern definitions
I was confused by the definition of `println`:

```
extern def println(value: String): Unit = ...
```

There is no capture annotation and I briefly thought the default is `pure`, but it is `io`, which is good.
However, we could make the capture annotation mandatory to users would have to write:

```
extern def println(value: String) at io: Unit =
```

Pull request description:
Fixes #1138

Patch:
diff --git a/effekt/jvm/src/main/scala/effekt/Server.scala b/effekt/jvm/src/main/scala/effekt/Server.scala
index 378ed435a..0f887ba67 100644
--- a/effekt/jvm/src/main/scala/effekt/Server.scala
+++ b/effekt/jvm/src/main/scala/effekt/Server.scala
@@ -26,6 +26,7 @@ import java.net.ServerSocket
 import java.nio.file.Paths
 import java.util
 import java.util.concurrent.{CompletableFuture, ExecutorService, Executors}
+import kiama.util.Range
 
 /**
  * Effekt Language Server
@@ -385,6 +386,8 @@ class Server(config: EffektConfig, compileOnChange: Boolean=false) extends Langu
             inlayHint.setTooltip(markup)
             inlayHint.setPaddingLeft(true)
             inlayHint.setData("capture")
+            val textEdit = new TextEdit(convertRange(Range(p, p)), s" $codeEdit")
+            inlayHint.setTextEdits(Collections.seqToJavaList(List(textEdit)))
             inlayHint
         }.toVector
 
diff --git a/effekt/shared/src/main/scala/effekt/Intelligence.scala b/effekt/shared/src/main/scala/effekt/Intelligence.scala
index c94d13f4e..669b3572e 100644
--- a/effekt/shared/src/main/scala/effekt/Intelligence.scala
+++ b/effekt/shared/src/main/scala/effekt/Intelligence.scala
@@ -320,12 +320,16 @@ trait Intelligence {
       // keep only captures in the current range
       case (t, c) => t.span.origin != Missing && t.span.range.from.between(range.from, range.to)
     }.collect {
-      case (t: source.FunDef, c) => for {
+      case (t: source.FunDef, c) if t.captures.isEmpty => for {
         pos <- if t.span.origin != Missing then Some(t.ret.span.range.from) else None
       } yield CaptureInfo(pos, c)
       case (t: source.DefDef, c) => for {
         pos <- if t.span.origin != Missing then Some(t.annot.span.range.from) else None
       } yield CaptureInfo(pos, c)
+      // only show it if it is the default capture inserted by the parser
+      case (t @ source.ExternDef(_, _, _, _, source.CaptureSet(_, Span(_, _, _, source.Origin.Synthesized)), _, _, _, _), c) => for {
+        pos <- if t.span.origin != Missing then Some(t.captures.span.range.from) else None
+      } yield CaptureInfo(pos, c)
       case (source.Box(Maybe(None, span), block, _), _) if C.inferredCaptureOption(block).isDefined => for {
         capt <- C.inferredCaptureOption(block)
       } yield CaptureInfo(span.range.from, capt)
diff --git a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala b/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala
index 8ec6925a3..f138221c4 100644
--- a/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala
+++ b/effekt/shared/src/main/scala/effekt/source/AnnotateCaptures.scala
@@ -99,6 +99,12 @@ object AnnotateCaptures extends Phase[Typechecked, Typechecked], Query[Unit, Cap
       val annotated = sym.caps.getOrElse(inferred)
       Context.annotate(Annotations.Captures, sym, annotated)
       annotated
+    case tree @ ExternDef(id, tparams, vparams, bparams, captures, ret, bodies, info, span) =>
+      val sym = tree.symbol
+      val capts = sym.capture
+      bodies.foreach { query }
+      Context.annotate(Annotations.Captures, sym, capts)
+      capts
     // regions
     case tree @ RegDef(id, annot, region, binding, doc, span) =>
       val regSymbol = region.symbol.asBlockSymbol


Test patch:
diff --git a/effekt/jvm/src/test/scala/effekt/LSPTests.scala b/effekt/jvm/src/test/scala/effekt/LSPTests.scala
index 784c2e55d..9af7e5e5b 100644
--- a/effekt/jvm/src/test/scala/effekt/LSPTests.scala
+++ b/effekt/jvm/src/test/scala/effekt/LSPTests.scala
@@ -719,6 +719,12 @@ class LSPTests extends FunSuite {
       inlayHint.setTooltip(markup)
       inlayHint.setPaddingLeft(true)
       inlayHint.setData("capture")
+      inlayHint.setTextEdits(
+        List(
+          new TextEdit(new Range(positions(1), positions(1)), " at {io}")
+        ).asJava
+      )
+
 
       val expectedInlayHints = List(inlayHint)
 
@@ -756,7 +762,11 @@ class LSPTests extends FunSuite {
       inlayHint.setTooltip(markup)
       inlayHint.setPaddingLeft(true)
       inlayHint.setData("capture")
-
+      inlayHint.setTextEdits(
+        List(
+          new TextEdit(new Range(positions(1), positions(1)), " at {io}")
+        ).asJava
+      )
       val expectedInlayHints = List(inlayHint)
 
       val didOpenParams = new DidOpenTextDocumentParams()
@@ -791,6 +801,11 @@ class LSPTests extends FunSuite {
       captureHint.setData("capture")
       captureHint.setTooltip(new MarkupContent("markdown", "captures: `{}`"))
       captureHint.setPaddingLeft(true)
+      captureHint.setTextEdits(
+        List(
+          new TextEdit(new Range(positions(1), positions(1)), " at {}")
+        ).asJava
+      )
 
       val omittedHint = new InlayHint()
       omittedHint.setKind(InlayHintKind.Type)
@@ -817,7 +832,7 @@ class LSPTests extends FunSuite {
       params.setRange(new Range(positions(0), positions(2)))
 
       val inlayHints = server.getTextDocumentService().inlayHint(params).get()
-      assertEquals(expectedInlayHints.asJava, inlayHints)
+      assertEquals(inlayHints, expectedInlayHints.asJava)
     }
   }
 
@@ -843,6 +858,11 @@ class LSPTests extends FunSuite {
       inlayHint.setTooltip(markup)
       inlayHint.setPaddingLeft(true)
       inlayHint.setData("capture")
+      inlayHint.setTextEdits(
+        List(
+          new TextEdit(new Range(positions(1), positions(1)), " at {io}")
+        ).asJava
+      )
 
       val expectedInlayHints = List(inlayHint)
 
@@ -881,6 +901,11 @@ class LSPTests extends FunSuite {
       paramsAfterChange.setRange(new Range(positions(0), new Position(positions(2).getLine + 1, positions(2).getCharacter)))
 
       inlayHint.setPosition(newPos)
+      inlayHint.setTextEdits(
+        List(
+          new TextEdit(new Range(newPos, newPos), " at {io}")
+        ).asJava
+      )
       val inlayHintsAfterChange = server.getTextDocumentService().inlayHint(paramsAfterChange).get()
       assertEquals(inlayHintsAfterChange, expectedInlayHints.asJava)
 
@@ -900,6 +925,11 @@ class LSPTests extends FunSuite {
       paramsAfterRevert.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier)
       paramsAfterRevert.setRange(new Range(positions(0), positions(2)))
 
+      inlayHint.setTextEdits(
+        List(
+          new TextEdit(new Range(positions(1), positions(1)), " at {io}")
+        ).asJava
+      )
       val inlayHintsAfterRevert = server.getTextDocumentService().inlayHint(paramsAfterRevert).get()
       assertEquals(inlayHintsAfterRevert, expectedInlayHints.asJava)
     }
@@ -928,6 +958,11 @@ class LSPTests extends FunSuite {
       inlayHint.setTooltip(markup)
       inlayHint.setPaddingLeft(true)
       inlayHint.setData("capture")
+      inlayHint.setTextEdits(
+        List(
+          new TextEdit(new Range(positions(1), positions(1)), " at {io}")
+        ).asJava
+      )
 
       val expectedInlayHints = List(inlayHint)