Skip to content

Commit 9ba3cfa

Browse files
timsueberkruebb-studios
authored andcommitted
Fix lsp caching issues (effekt-lang#894)
Depends on effekt-lang#885 (mostly so we can actually express the unit test). I split `AnnotationsDB` into three separate databases which each tracks its own map. This allows us to properly distinguish types that should be annotated by their object identity (symbols, trees) and those that should be annotated by their value identity (sources). The new types are: * `TreeAnnotations`: key: `source.Tree`, object identity * `SourceAnnotations`: key: `kiama.util.Source`, value identity * `SymbolAnnotations`: key: `symbols.Symbol`, object identity Fixes effekt-lang#876. --------- Co-authored-by: Jonathan Brachthäuser <[email protected]>
1 parent 67f47cb commit 9ba3cfa

File tree

4 files changed

+435
-141
lines changed

4 files changed

+435
-141
lines changed

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

Lines changed: 234 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,12 @@ class LSPTests extends FunSuite {
2020
//
2121
//
2222

23-
def withClientAndServer(testBlock: (MockLanguageClient, Server) => Unit): Unit = {
23+
24+
/**
25+
* @param compileOnChange The server currently uses `compileOnChange = false` by default, but we set it to `true` for testing
26+
* because we would like to switch to `didChange` events once we have working caching for references.
27+
*/
28+
def withClientAndServer(compileOnChange: Boolean)(testBlock: (MockLanguageClient, Server) => Unit): Unit = {
2429
val driver = new Driver {}
2530
val config = EffektConfig(Seq("--server"))
2631
config.verify()
@@ -30,9 +35,7 @@ class LSPTests extends FunSuite {
3035
val serverIn = new PipedInputStream(clientOut)
3136
val serverOut = new PipedOutputStream(clientIn)
3237

33-
// The server currently uses `compileOnChange = false` by default, but we set it to `true` for testing
34-
// because we would like to switch to `didChange` events once we have working caching for references.
35-
val server = new Server(config, compileOnChange = true)
38+
val server = new Server(config, compileOnChange)
3639

3740
val mockClient = new MockLanguageClient()
3841
server.connect(mockClient)
@@ -42,7 +45,11 @@ class LSPTests extends FunSuite {
4245
testBlock(mockClient, server)
4346
}
4447

45-
// Fixtures
48+
def withClientAndServer(testBlock: (MockLanguageClient, Server) => Unit): Unit = {
49+
withClientAndServer(true)(testBlock)
50+
}
51+
52+
// Fixtures
4653
//
4754
//
4855

@@ -343,6 +350,78 @@ class LSPTests extends FunSuite {
343350
}
344351
}
345352

353+
test("Hovering works after editing") {
354+
withClientAndServer { (client, server) =>
355+
// Initial code
356+
//
357+
//
358+
359+
val (textDoc, firstPos) = raw"""
360+
|val x: Int = 42
361+
| ↑
362+
|""".textDocumentAndPosition
363+
val hoverContents =
364+
raw"""|#### Value binder
365+
|```effekt
366+
|test::x: Int
367+
|```
368+
|""".stripMargin
369+
370+
val didOpenParams = new DidOpenTextDocumentParams()
371+
didOpenParams.setTextDocument(textDoc)
372+
server.getTextDocumentService().didOpen(didOpenParams)
373+
374+
val hoverParams = new HoverParams(textDoc.versionedTextDocumentIdentifier, firstPos)
375+
val hover = server.getTextDocumentService().hover(hoverParams).get()
376+
377+
val expectedHover = (pos: Position) => {
378+
val expectedHover = new Hover()
379+
expectedHover.setRange(new Range(pos, pos))
380+
expectedHover.setContents(new MarkupContent("markdown", hoverContents))
381+
expectedHover
382+
}
383+
assertEquals(hover, expectedHover(firstPos))
384+
385+
// First edit: now we add a blank line in front
386+
//
387+
//
388+
389+
val (newTextDoc, changeEvent) = textDoc.changeTo(
390+
raw"""
391+
|
392+
|val x: Int = 42
393+
|""".stripMargin
394+
)
395+
val secondPos = new Position(firstPos.getLine + 1, firstPos.getCharacter)
396+
397+
val didChangeParams = new DidChangeTextDocumentParams()
398+
didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier)
399+
didChangeParams.setContentChanges(util.Arrays.asList(changeEvent))
400+
server.getTextDocumentService().didChange(didChangeParams)
401+
402+
val hoverParamsAfterChange = new HoverParams(newTextDoc.versionedTextDocumentIdentifier, secondPos)
403+
val hoverAfterChange = server.getTextDocumentService().hover(hoverParamsAfterChange).get()
404+
405+
assertEquals(hoverAfterChange, expectedHover(secondPos))
406+
407+
// Second edit: we revert the change
408+
//
409+
//
410+
411+
val (revertedTextDoc, revertedChangeEvent) = newTextDoc.changeTo(textDoc.getText)
412+
413+
val didChangeParamsReverted = new DidChangeTextDocumentParams()
414+
didChangeParamsReverted.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier)
415+
didChangeParamsReverted.setContentChanges(util.Arrays.asList(revertedChangeEvent))
416+
server.getTextDocumentService().didChange(didChangeParamsReverted)
417+
418+
val hoverParamsAfterRevert = new HoverParams(revertedTextDoc.versionedTextDocumentIdentifier, firstPos)
419+
val hoverAfterRevert = server.getTextDocumentService().hover(hoverParamsAfterRevert).get()
420+
421+
assertEquals(hoverAfterRevert, expectedHover(firstPos))
422+
}
423+
}
424+
346425
// LSP: Document symbols
347426
//
348427
//
@@ -487,6 +566,156 @@ class LSPTests extends FunSuite {
487566
}
488567
}
489568

569+
test("inlayHints work after editing") {
570+
withClientAndServer { (client, server) =>
571+
val (textDoc, positions) =
572+
raw"""
573+
|↑
574+
|def main() = {
575+
|↑
576+
| println("Hello, world!")
577+
|}
578+
|↑
579+
|""".textDocumentAndPositions
580+
581+
val inlayHint = new InlayHint()
582+
inlayHint.setKind(InlayHintKind.Type)
583+
inlayHint.setPosition(positions(1))
584+
inlayHint.setLabel("{io}")
585+
val markup = new MarkupContent()
586+
markup.setKind("markdown")
587+
markup.setValue("captures: `{io}`")
588+
inlayHint.setTooltip(markup)
589+
inlayHint.setPaddingRight(true)
590+
inlayHint.setData("capture")
591+
592+
val expectedInlayHints = List(inlayHint)
593+
594+
val didOpenParams = new DidOpenTextDocumentParams()
595+
didOpenParams.setTextDocument(textDoc)
596+
server.getTextDocumentService().didOpen(didOpenParams)
597+
598+
val params = new InlayHintParams()
599+
params.setTextDocument(textDoc.versionedTextDocumentIdentifier)
600+
params.setRange(new Range(positions(0), positions(2)))
601+
602+
val inlayHints = server.getTextDocumentService().inlayHint(params).get()
603+
assertEquals(inlayHints, expectedInlayHints.asJava)
604+
605+
// First edit: now we add a blank line in front
606+
//
607+
//
608+
609+
val (newTextDoc, changeEvent) = textDoc.changeTo(
610+
raw"""
611+
|
612+
|def main() = {
613+
| println("Hello, world!")
614+
|}
615+
|""".stripMargin
616+
)
617+
val newPos = new Position(positions(1).getLine + 1, positions(1).getCharacter)
618+
619+
val didChangeParams = new DidChangeTextDocumentParams()
620+
didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier)
621+
didChangeParams.setContentChanges(util.Arrays.asList(changeEvent))
622+
server.getTextDocumentService().didChange(didChangeParams)
623+
624+
val paramsAfterChange = new InlayHintParams()
625+
paramsAfterChange.setTextDocument(newTextDoc.versionedTextDocumentIdentifier)
626+
paramsAfterChange.setRange(new Range(positions(0), new Position(positions(2).getLine + 1, positions(2).getCharacter)))
627+
628+
inlayHint.setPosition(newPos)
629+
val inlayHintsAfterChange = server.getTextDocumentService().inlayHint(paramsAfterChange).get()
630+
assertEquals(inlayHintsAfterChange, expectedInlayHints.asJava)
631+
632+
// Second edit: we revert the change
633+
//
634+
//
635+
636+
val (revertedTextDoc, revertedChangeEvent) = newTextDoc.changeTo(textDoc.getText)
637+
inlayHint.setPosition(positions(1))
638+
639+
val didChangeParamsReverted = new DidChangeTextDocumentParams()
640+
didChangeParamsReverted.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier)
641+
didChangeParamsReverted.setContentChanges(util.Arrays.asList(revertedChangeEvent))
642+
server.getTextDocumentService().didChange(didChangeParamsReverted)
643+
644+
val paramsAfterRevert = new InlayHintParams()
645+
paramsAfterRevert.setTextDocument(revertedTextDoc.versionedTextDocumentIdentifier)
646+
paramsAfterRevert.setRange(new Range(positions(0), positions(2)))
647+
648+
val inlayHintsAfterRevert = server.getTextDocumentService().inlayHint(paramsAfterRevert).get()
649+
assertEquals(inlayHintsAfterRevert, expectedInlayHints.asJava)
650+
}
651+
652+
}
653+
654+
test("inlayHints work after invalid edits") {
655+
withClientAndServer(false) { (client, server) =>
656+
val (textDoc, positions) =
657+
raw"""
658+
|↑
659+
|def main() = {
660+
|↑
661+
| println("Hello, world!")
662+
|}
663+
|↑
664+
|""".textDocumentAndPositions
665+
666+
val inlayHint = new InlayHint()
667+
inlayHint.setKind(InlayHintKind.Type)
668+
inlayHint.setPosition(positions(1))
669+
inlayHint.setLabel("{io}")
670+
val markup = new MarkupContent()
671+
markup.setKind("markdown")
672+
markup.setValue("captures: `{io}`")
673+
inlayHint.setTooltip(markup)
674+
inlayHint.setPaddingRight(true)
675+
inlayHint.setData("capture")
676+
677+
val expectedInlayHints = List(inlayHint)
678+
679+
val didOpenParams = new DidOpenTextDocumentParams()
680+
didOpenParams.setTextDocument(textDoc)
681+
server.getTextDocumentService().didOpen(didOpenParams)
682+
683+
val params = new InlayHintParams()
684+
params.setTextDocument(textDoc.versionedTextDocumentIdentifier)
685+
params.setRange(new Range(positions(0), positions(2)))
686+
687+
val inlayHints = server.getTextDocumentService().inlayHint(params).get()
688+
assertEquals(inlayHints, expectedInlayHints.asJava)
689+
690+
// Edit: now we add some invalid syntax to the end
691+
//
692+
//
693+
694+
val (newTextDoc, changeEvent) = textDoc.changeTo(
695+
raw"""
696+
|def main() = {
697+
| println("Hello, world!")
698+
|}
699+
|invalid syntax
700+
|""".stripMargin
701+
)
702+
703+
val didChangeParams = new DidChangeTextDocumentParams()
704+
didChangeParams.setTextDocument(newTextDoc.versionedTextDocumentIdentifier)
705+
didChangeParams.setContentChanges(util.Arrays.asList(changeEvent))
706+
server.getTextDocumentService().didChange(didChangeParams)
707+
708+
val paramsAfterChange = new InlayHintParams()
709+
paramsAfterChange.setTextDocument(newTextDoc.versionedTextDocumentIdentifier)
710+
// The client may send a range that is outside of the text the server currently has
711+
// We use somewhat arbitrary values here.
712+
paramsAfterChange.setRange(new Range(positions(0), new Position(positions(2).getLine + 1, positions(2).getCharacter + 5)))
713+
714+
val inlayHintsAfterChange = server.getTextDocumentService().inlayHint(paramsAfterChange).get()
715+
assertEquals(inlayHintsAfterChange, expectedInlayHints.asJava)
716+
}
717+
}
718+
490719
// Effekt: Publish IR
491720
//
492721
//

0 commit comments

Comments
 (0)