diff --git a/.drone.yml b/.drone.yml index 07ee3a45f974..4d063a8a49ef 100644 --- a/.drone.yml +++ b/.drone.yml @@ -43,11 +43,18 @@ pipeline: - cp -R . /tmp/3/ && cd /tmp/3/ - ./project/scripts/sbt dotty-optimised/test - test_sbt: + test_ide: group: test image: lampepfl/dotty:2018-04-10 commands: - cp -R . /tmp/4/ && cd /tmp/4/ + - ./project/scripts/sbt dotty-language-server/test + + test_sbt: + group: test + image: lampepfl/dotty:2017-11-17 + commands: + - cp -R . /tmp/5/ && cd /tmp/5/ - ./project/scripts/sbt sbt-dotty/scripted when: # sbt scripted tests are slow and only run on nightly or deployment diff --git a/compiler/src/dotty/tools/dotc/util/DiffUtil.scala b/compiler/src/dotty/tools/dotc/util/DiffUtil.scala index 12a56a771b47..ea49ef2391f1 100644 --- a/compiler/src/dotty/tools/dotc/util/DiffUtil.scala +++ b/compiler/src/dotty/tools/dotc/util/DiffUtil.scala @@ -59,6 +59,22 @@ object DiffUtil { (fnd, exp, totalChange.toDouble / (expected.length + found.length)) } + /** + * Return a colored diff between the tokens of every line in `expected` and `actual`. Each line of + * output contains the expected value on the left and the actual value on the right. + * + * @param expected The expected lines + * @param actual The actual lines + * @return A string with one element of `expected` and `actual` on each lines, where + * differences are highlighted. + */ + def mkColoredLineDiff(expected: Seq[String], actual: Seq[String]): String = { + val expectedSize = EOF.length max expected.maxBy(_.length).length + actual.padTo(expected.length, "").zip(expected.padTo(actual.length, "")).map { case (act, exp) => + mkColoredLineDiff(exp, act, expectedSize) + }.mkString(System.lineSeparator) + } + def mkColoredLineDiff(expected: String, actual: String, expectedSize: Int): String = { lazy val diff = { val tokens = splitTokens(expected, Nil).toArray diff --git a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala index 9a3a2b093a57..945d5fa3efb4 100644 --- a/compiler/test/dotty/tools/vulpix/ParallelTesting.scala +++ b/compiler/test/dotty/tools/vulpix/ParallelTesting.scala @@ -612,10 +612,7 @@ trait ParallelTesting extends RunnerOrchestration { self => if (outputLines.length != checkLines.length || !linesMatch) { // Print diff to files and summary: - val expectedSize = DiffUtil.EOF.length max checkLines.map(_.length).max - val diff = outputLines.padTo(checkLines.length, "").zip(checkLines.padTo(outputLines.length, "")).map { case (act, exp) => - DiffUtil.mkColoredLineDiff(exp, act, expectedSize) - }.mkString("\n") + val diff = DiffUtil.mkColoredLineDiff(checkLines, outputLines) val msg = s"""|Output from '$sourceTitle' did not match check file. diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index fe8954760ddc..3341566f600f 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -303,8 +303,9 @@ class DottyLanguageServer extends LanguageServer val newName = params.getNewName val refs = Interactive.namedTrees(trees, includeReferences = true, tree => - (Interactive.matchSymbol(tree, sym, Include.overriding) - || (linkedSym != NoSymbol && Interactive.matchSymbol(tree, linkedSym, Include.overriding)))) + tree.pos.isSourceDerived + && (Interactive.matchSymbol(tree, sym, Include.overriding) + || (linkedSym != NoSymbol && Interactive.matchSymbol(tree, linkedSym, Include.overriding)))) val changes = refs.groupBy(ref => toUri(ref.source).toString).mapValues(_.map(ref => new TextEdit(range(ref.namePos), newName)).asJava) diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala new file mode 100644 index 000000000000..2e33f77d040c --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -0,0 +1,14 @@ +package dotty.tools.languageserver + +import org.junit.Test +import org.eclipse.lsp4j.CompletionItemKind + +import dotty.tools.languageserver.util.Code._ + +class CompletionTest { + + @Test def completion0: Unit = { + code"class Foo { val xyz: Int = 0; def y: Int = xy$m1 }".withSource + .completion(m1, Set(("xyz", CompletionItemKind.Field, "Int"))) + } +} diff --git a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala new file mode 100644 index 000000000000..cf18ea0914b8 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala @@ -0,0 +1,39 @@ +package dotty.tools.languageserver + +import org.junit.Test + +import dotty.tools.languageserver.util.Code._ + +class DefinitionTest { + + @Test def classDefinitionNotFound0: Unit = + code"class Foo { new ${m1}Bar$m2 }".withSource.definition(m1 to m2, Nil) + + @Test def classDefinition0: Unit = { + withSources( + code"class ${m1}Foo$m2 { new Foo }", + code"class Bar { val foo: ${m3}Foo$m4 = new ${m5}Foo$m6 }" + ) .definition(m1 to m2, List(m1 to m2)) + .definition(m3 to m4, List(m1 to m2)) + .definition(m5 to m6, List(m1 to m2)) + } + + @Test def valDefinition0: Unit = { + withSources( + code"class Foo { val ${m1}x$m2 = 0; ${m3}x$m4 }", + code"class Bar { val foo = new Foo; foo.${m5}x$m6 }" + ) .definition(m1 to m2, List(m1 to m2)) + .definition(m3 to m4, List(m1 to m2)) + .definition(m5 to m6, List(m1 to m2)) + } + + @Test def defDefinition0: Unit = { + withSources( + code"class Foo { def ${m1}x$m2 = 0; ${m3}x$m4 }", + code"class Bar { val foo = new Foo; foo.${m5}x$m6 }" + ) .definition(m1 to m2, List(m1 to m2)) + .definition(m3 to m4, List(m1 to m2)) + .definition(m5 to m6, List(m1 to m2)) + } + +} diff --git a/language-server/test/dotty/tools/languageserver/DocumentSymbolTest.scala b/language-server/test/dotty/tools/languageserver/DocumentSymbolTest.scala new file mode 100644 index 000000000000..6add328b57dd --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/DocumentSymbolTest.scala @@ -0,0 +1,25 @@ +package dotty.tools.languageserver + +import org.junit.Test +import org.eclipse.lsp4j.SymbolKind + +import dotty.tools.languageserver.util.Code._ + + +class DocumentSymbolTest { + + @Test def documentSymbol0: Unit = + code"class ${m1}Foo$m2".withSource.documentSymbol(m1, (m1 to m2).symInfo("Foo", SymbolKind.Class)) + + @Test def documentSymbol1: Unit = + code"class ${m1}Foo$m2; class ${m3}Bar$m4".withSource + .documentSymbol(m1, (m1 to m2).symInfo("Foo", SymbolKind.Class), (m3 to m4).symInfo("Bar", SymbolKind.Class)) + + @Test def documentSymbol3: Unit = { + withSources( + code"class ${m1}Foo$m2", + code"class ${m3}Bar$m4" + ) .documentSymbol(m1, (m1 to m2).symInfo("Foo", SymbolKind.Class)) + .documentSymbol(m3, (m3 to m4).symInfo("Bar", SymbolKind.Class)) + } +} diff --git a/language-server/test/dotty/tools/languageserver/HighlightTest.scala b/language-server/test/dotty/tools/languageserver/HighlightTest.scala new file mode 100644 index 000000000000..a863197fdf2e --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/HighlightTest.scala @@ -0,0 +1,22 @@ +package dotty.tools.languageserver + +import org.junit.Test +import dotty.tools.languageserver.util.Code._ +import org.eclipse.lsp4j.DocumentHighlightKind + +class HighlightTest { + + @Test def valHighlight0: Unit = { + val xDef = (m1 to m2).withCode("x") + code"class X { val $xDef = 9 }".withSource + .highlight(xDef.range, (xDef.range, DocumentHighlightKind.Read)) + } + + @Test def valHighlight1: Unit = { + val xDef = (m1 to m2).withCode("x") + val xRef = (m3 to m4).withCode("x") + code"class X { val $xDef = 9; $xRef}".withSource + .highlight(xRef.range, (xDef.range, DocumentHighlightKind.Read), (xRef.range, DocumentHighlightKind.Read)) + } + +} diff --git a/language-server/test/dotty/tools/languageserver/HoverTest.scala b/language-server/test/dotty/tools/languageserver/HoverTest.scala new file mode 100644 index 000000000000..34dac2ce0b91 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/HoverTest.scala @@ -0,0 +1,74 @@ +package dotty.tools.languageserver + +import org.junit.Test + +import dotty.tools.languageserver.util.Code._ + +class HoverTest { + + @Test def hoverOnWhiteSpace0: Unit = + code"$m1 $m2".withSource.hover(m1 to m2, "") + + @Test def hoverOnClass0: Unit = { + code"""$m1 ${m2}class Foo $m3 $m4""".withSource + .hover(m1 to m2, "") + .hover(m2 to m3, "Foo") + .hover(m3 to m4, "") + } + + @Test def hoverOnClass1: Unit = { + code"""$m1 ${m2}class Foo { } $m3 $m4""".withSource + .hover(m1 to m2, "") + .hover(m2 to m3, "Foo") + .hover(m3 to m4, "") + } + + @Test def hoverOnValDef0: Unit = { + code"""class Foo { + | ${m1}val x = ${m2}8$m3; ${m4}x$m5 + |}""".withSource + .hover(m1 to m2, "Int") + .hover(m2 to m3, "Int(8)") + .hover(m4 to m5, "Int") + } + + @Test def hoverOnValDef1: Unit = { + code"""class Foo { + | ${m1}final val x = 8$m2; ${m3}x$m4 + |}""".withSource + .hover(m1 to m2, "Int(8)") + .hover(m3 to m4, "Int(8)") + } + + @Test def hoverOnDefDef0: Unit = { + code"""class Foo { + | ${m1}def x = ${m2}8$m3; ${m4}x$m5 + |}""".withSource + .hover(m1 to m2, "Int") + .hover(m2 to m3, "Int(8)") + .hover(m4 to m5, "Int") + } + + @Test def hoverMissingRef0: Unit = { + code"""class Foo { + | ${m1}x$m2 + |}""".withSource + .hover(m1 to m2, "") + } + + @Test def hoverFun0: Unit = { + code"""class Foo { + | def x: String = $m1"abc"$m2 + | ${m3}x$m4 + | + | def y(): Int = 9 + | ${m5}y($m6)$m7 + |} + """.withSource + .hover(m1 to m2, "String(\"abc\")") + .hover(m3 to m4, "String") + .hover(m5 to m6, "(): Int") + .hover(m6 to m7, "Int") + } + +} diff --git a/language-server/test/dotty/tools/languageserver/ReferencesTest.scala b/language-server/test/dotty/tools/languageserver/ReferencesTest.scala new file mode 100644 index 000000000000..427b2d244d5f --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/ReferencesTest.scala @@ -0,0 +1,22 @@ +package dotty.tools.languageserver + +import org.junit.Test + +import dotty.tools.languageserver.util.Code._ + +class ReferencesTest { + + @Test def valNoReferences0: Unit = + code"class X { val ${m1}x$m2 = 9 }".withSource.references(m1 to m2, Nil) + + @Test def valReferences0: Unit = { + code"class X { val ${m1}x$m2 = 9; ${m3}x$m4; ${m5}x$m6 }".withSource + .references(m1 to m2, List(m3 to m4, m5 to m6)) + } + + @Test def valReferences1: Unit = { + code"class X { val ${m1}x$m2 = 9; ${m3}x$m4; ${m5}x$m6 }".withSource + .references(m1 to m2, List(m1 to m2, m3 to m4, m5 to m6), withDecl = true) + } + +} diff --git a/language-server/test/dotty/tools/languageserver/RenameTest.scala b/language-server/test/dotty/tools/languageserver/RenameTest.scala new file mode 100644 index 000000000000..a349e417bdca --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/RenameTest.scala @@ -0,0 +1,30 @@ +package dotty.tools.languageserver + +import org.junit.Test + +import dotty.tools.languageserver.util.Code._ +import dotty.tools.languageserver.util.embedded.CodeMarker + +class RenameTest { + + @Test def rename0: Unit = { + def testRenameFrom(m: CodeMarker) = + code"class ${m1}Foo$m2 { new ${m3}Foo$m4 }".withSource.rename(m, "Bar", Set(m1 to m2, m3 to m4)) + testRenameFrom(m1) + testRenameFrom(m3) + } + + + @Test def rename1: Unit = { + def testRenameFrom(m: CodeMarker) = + withSources( + code"class ${m1}Foo$m2 { new ${m3}Foo$m4 }", + code"class Bar { new ${m5}Foo$m6 }" + ).rename(m, "Bar", Set(m1 to m2, m3 to m4, m5 to m6)) + + testRenameFrom(m1) + testRenameFrom(m3) + testRenameFrom(m5) + } + +} diff --git a/language-server/test/dotty/tools/languageserver/SymbolTest.scala b/language-server/test/dotty/tools/languageserver/SymbolTest.scala new file mode 100644 index 000000000000..5a7cb73af6df --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/SymbolTest.scala @@ -0,0 +1,30 @@ +package dotty.tools.languageserver + +import org.junit.Test +import org.eclipse.lsp4j.SymbolKind + +import dotty.tools.languageserver.util.Code._ + +class SymbolTest { + + @Test def symbol0: Unit = { + val Foo = (m1 to m2).withCode("Foo") + withSources(code"class $Foo").symbol("Foo", Foo.range.symInfo("Foo", SymbolKind.Class)) + } + + @Test def symbol1: Unit = { + val Foo = (m1 to m2).withCode("Foo") + val Bar = (m3 to m4).withCode("Bar") + val fooFoo = (m5 to m6).withCode("Foo") + withSources( + code"class $Foo", + code"class $Bar", + code"""package foo + |class $fooFoo { + | class Bar + |} + """ + ) .symbol("Foo", Foo.range.symInfo("Foo", SymbolKind.Class), fooFoo.range.symInfo("Foo", SymbolKind.Class, "foo")) + .symbol("Bar", Bar.range.symInfo("Bar", SymbolKind.Class)) + } +} diff --git a/language-server/test/dotty/tools/languageserver/util/Code.scala b/language-server/test/dotty/tools/languageserver/util/Code.scala new file mode 100644 index 000000000000..dc43e43c57d8 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/Code.scala @@ -0,0 +1,91 @@ +package dotty.tools.languageserver.util + +import dotty.tools.languageserver.util.embedded._ + +/** + * Helper object to create virtual source files and extract markers from the source files, using the + * `code` interpolator. + * + * The markers can then be used to perform tests at different positions within the source + * file. + */ +object Code { + + // Default positions + val m1 = new CodeMarker("m1") + val m2 = new CodeMarker("m2") + val m3 = new CodeMarker("m3") + val m4 = new CodeMarker("m4") + val m5 = new CodeMarker("m5") + val m6 = new CodeMarker("m6") + val m7 = new CodeMarker("m7") + val m8 = new CodeMarker("m8") + + implicit class CodeHelper(val sc: StringContext) extends AnyVal { + + /** + * An interpolator that lets set marker inside a virtual source file. + * + * For instance: + * ``` + * code"""object ${m1}Foo${m2} { def bar = ${m3}Hello{$m4}.quux }""" + * ``` + * + * This will define a source file where the markers `m1` and `m2` enclose the identifier `Foo`, + * and `m3` and `m4` enclose the identifier `Hello`. These positions can then be used to ask to + * perform actions such as finding all references, etc. + */ + def code(args: Embedded*): SourceWithPositions = { + val pi = sc.parts.iterator + val ai = args.iterator + + var line = 0 + var char = 0 + def scan(str: String): Unit = { + for (c <- str) + if (c == '\n') { line += 1; char = 0 } else { char += 1 } + } + + val stringBuilder = new StringBuilder + val positions = List.newBuilder[(CodeMarker, Int, Int)] + + while (ai.hasNext) { + val next = pi.next().stripMargin + stringBuilder.append(next) + scan(next) + + ai.next() match { + case emb: CodeMarker => + positions += ((emb, line, char)) + + case emb: CodeInRange => + positions += ((emb.range.start, line, char)) + scan(emb.text) + stringBuilder.append(emb.text) + positions += ((emb.range.end, line, char)) + } + + } + + if (pi.hasNext) + stringBuilder.append(pi.next()) + + SourceWithPositions(stringBuilder.result(), positions.result()) + } + } + + /** A new `CodeTester` working with `sources` in the workspace. */ + def withSources(sources: SourceWithPositions*): CodeTester = new CodeTester(sources.toList, Nil) + + /** + * A virtual source file where several markers have been set. + * + * @param text The code contained within the virtual source file. + * @param positions The positions of the markers that have been set. + */ + case class SourceWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) { + /** A new `CodeTester` with only this source in the workspace. */ + def withSource: CodeTester = new CodeTester(this :: Nil, Nil) + } + +} diff --git a/language-server/test/dotty/tools/languageserver/util/CodeRange.scala b/language-server/test/dotty/tools/languageserver/util/CodeRange.scala new file mode 100644 index 000000000000..60a965955f21 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/CodeRange.scala @@ -0,0 +1,49 @@ +package dotty.tools.languageserver.util + +import dotty.tools.languageserver.util.embedded.{CodeInRange, CodeMarker} +import dotty.tools.languageserver.util.server.TestFile + +import org.eclipse.lsp4j._ + +import PositionContext._ + +/** + * A range of positions between two markers. + * + * @param start The start marker. + * @param end The end marker. + */ +case class CodeRange(start: CodeMarker, end: CodeMarker) { + private[this] var checked = false + def check(): PosCtx[Unit] = { + if (!checked) { + assert(start.file == end.file, s"$start and $end where not in the same file") + assert(start.line <= end.line, s"Expected $end to be after $start") + assert(start.line != end.line || start.character < end.character, s"Expected $end to be after $start") + checked = true + } + } + + def file: PosCtx[TestFile] = { + check() + start.file + } + + def withCode(text: String): CodeInRange = CodeInRange(text, this) + + def symInfo(name: String, kind: SymbolKind, container: String = null): SymInfo = + new SymInfo(name, kind, this, container) + + def toRange: PosCtx[Range] = { + check() + new Range(start.toPosition, end.toPosition) + } + + def toLocation: PosCtx[Location] = { + check() + new Location(file.uri, toRange) + } + + def show: PosCtx[String] = + s"[start=${start.show}, end=${end.show}]" +} diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala new file mode 100644 index 000000000000..5d79ff733a86 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -0,0 +1,152 @@ +package dotty.tools.languageserver.util + +import dotty.tools.languageserver.util.Code.SourceWithPositions +import dotty.tools.languageserver.util.actions._ +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.server.{TestFile, TestServer} +import org.eclipse.lsp4j.{CompletionItemKind, DocumentHighlightKind} + +/** + * Simulates an LSP client for test in a workspace defined by `sources`. + * + * @param sources The list of sources in the workspace + * @param actions Unused + */ +class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) { + + private val testServer = new TestServer(TestFile.testDir) + + private val files = sources.zipWithIndex.map { case (code, i) => + testServer.openCode(code.text, s"Source$i.scala") + } + private val positions: PositionContext = getPositions(files) + + /** + * Perform a hover over `range`, verifies that result matches `expected`. + * + * @param range The range over which to hover. + * @param expected The expected result. + * + * @see dotty.tools.languageserver.util.actions.CodeHover + */ + def hover(range: CodeRange, expected: String): this.type = + doAction(new CodeHover(range, expected)) + + /** + * Perform a jump to definition over `range`, verifies that the results are `expected`. + * + * @param range The range of positions from which run `jump to definition`. + * @param expected The expected positions to jump to. + * + * @see dotty.tools.languageserver.util.actions.CodeDefinition + */ + def definition(range: CodeRange, expected: Seq[CodeRange]): this.type = + doAction(new CodeDefinition(range, expected)) + + /** + * Perform a highlight over `range`, verifies that the ranges and kinds of symbols match + * `expected`. + * + * @param range The range of positions to highlight. + * @param expected The expected ranges and the kind of symbols that should be highlighted. + * + * @see dotty.tools.languageserver.util.actions.CodeDefinition + */ + def highlight(range: CodeRange, expected: (CodeRange, DocumentHighlightKind)*): this.type = + doAction(new CodeDocumentHighlight(range, expected)) + + /** + * Finds all the references to the symbol in `range`, verifies that the results match `expected`. + * + * @param range The range of positions from which search for references. + * @param expected The expected positions of the references + * @param withDecl When set, include the declaration of the symbol under `range` in the results. + * + * @see dotty.tools.languageserver.util.actions.CodeReferences + */ + def references(range: CodeRange, expected: List[CodeRange], withDecl: Boolean = false): this.type = + doAction(new CodeReferences(range, expected, withDecl)) + + /** + * Requests completion at the position defined by `marker`, verifies that the results match + * `expected`. + * + * @param marker The position from which to ask for completions. + * @param expected The expected completion results. + * + * @see dotty.tools.languageserver.util.actions.CodeCompletion + */ + def completion(marker: CodeMarker, expected: Set[(String, CompletionItemKind, String)]): this.type = + doAction(new CodeCompletion(marker, expected)) + + /** + * Performs a workspace-wide renaming of the symbol under `marker`, verifies that the positions to + * update match `expected`. + * + * @param marker The position from which to ask for renaming. + * @param newName The new name to give to the symbol. + * @param expected The expected positions to change. + * + * @see dotty.tools.languageserver.util.actions.CodeRename + */ + def rename(marker: CodeMarker, newName: String, expected: Set[CodeRange]): this.type = + doAction(new CodeRename(marker, newName, expected)) // TODO apply changes to the sources and positions + + /** + * Queries for all the symbols referenced in the source file in `marker`, verifies that they match + * `expected`. + * + * @param marker The marker defining the source file from which to query. + * @param expected The expected symbols to be found. + * + * @see dotty.tools.languageserver.util.actions.CodeDocumentSymbol + */ + def documentSymbol(marker: CodeMarker, expected: SymInfo*): this.type = + doAction(new CodeDocumentSymbol(marker, expected)) + + /** + * Queries the whole workspace for symbols matching `query`, verifies that the results match + * `expected`. + * + * @param query The query used to find symbols. + * @param expected The expected symbols to be found. + * + * @see dotty.tools.languageserver.util.actions.CodeSymbol + */ + def symbol(query: String, symbols: SymInfo*): this.type = + doAction(new CodeSymbol(query, symbols)) + + private def doAction(action: Action): this.type = { + try { + action.execute()(testServer, positions) + } catch { + case ex: AssertionError => + val sourcesStr = sources.zip(files).map{ case (source, file) => "// " + file.file + "\n" + source.text}.mkString("\n") + val msg = + s""" + | + |$sourcesStr + | + |while executing action: ${action.show(positions)} + | + """.stripMargin + val assertionError = new AssertionError(msg + ex.getMessage) + assertionError.setStackTrace(ex.getStackTrace) + throw assertionError + } + this + } + + private def getPositions(files: List[TestFile]): PositionContext = { + val posSeq = { + for { + (code, file) <- sources.zip(files) + (position, line, char) <- code.positions + } yield position -> (file, line, char) + } + val posMap = posSeq.toMap + assert(posSeq.size == posMap.size, + "Each CodeMarker instance can only appear once in the code: " + posSeq.map(x => (x._1, x._2._2, x._2._3))) + new PositionContext(posMap) + } +} diff --git a/language-server/test/dotty/tools/languageserver/util/PositionContext.scala b/language-server/test/dotty/tools/languageserver/util/PositionContext.scala new file mode 100644 index 000000000000..45b97c886078 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/PositionContext.scala @@ -0,0 +1,28 @@ +package dotty.tools.languageserver.util + +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.server.TestFile + +class PositionContext(positionMap: Map[CodeMarker, (TestFile, Int, Int)]) { + private[this] var lastKey: CodeMarker = _ + private[this] var lastValue: (TestFile, Int, Int) = _ + def positionOf(pos: CodeMarker): (TestFile, Int, Int) = { + if (lastKey eq pos) lastValue + else { + lastValue = positionMap.getOrElse(pos, + { assert(false, "CodePosition was not found in the code: " + pos); null } + ) + lastKey = pos + lastValue + } + } + + def contains(pos: CodeMarker): Boolean = positionMap.contains(pos) + + def withPos(marker: CodeMarker, pos: (TestFile, Int, Int)) = + new PositionContext(positionMap.updated(marker, pos)) +} + +object PositionContext { + type PosCtx[T] = implicit PositionContext => T +} diff --git a/language-server/test/dotty/tools/languageserver/util/SymInfo.scala b/language-server/test/dotty/tools/languageserver/util/SymInfo.scala new file mode 100644 index 000000000000..64e5cc7ade31 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/SymInfo.scala @@ -0,0 +1,14 @@ +package dotty.tools.languageserver.util + +import dotty.tools.languageserver.util.PositionContext._ +import org.eclipse.lsp4j._ + +class SymInfo(name: String, kind: SymbolKind, range: CodeRange, container: String) { + def toSymInformation: PosCtx[SymbolInformation] = + new SymbolInformation(name, kind, range.toLocation, container) + + def show: PosCtx[String] = + s"SymInfo($name, $kind, ${range.show}, $container)" + override def toString: String = + s"SymInfo($name, $kind, $range, $container)" +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/Action.scala b/language-server/test/dotty/tools/languageserver/util/actions/Action.scala new file mode 100644 index 000000000000..06d0bed35a53 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/Action.scala @@ -0,0 +1,25 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.DottyLanguageServer +import dotty.tools.languageserver.util.PositionContext +import dotty.tools.languageserver.util.server.TestServer + +import PositionContext._ + +/** + * Base trait for representing an action performed against a language server (such as hover, go to + * definition, etc.) + */ +trait Action { + type Exec[T] = implicit (TestServer, PositionContext) => T + + /** Execute the action. */ + def execute(): Exec[Unit] + + /** Return a textual representation of this action. */ + def show: PosCtx[String] + + /** The server that this action targets. */ + def server: Exec[DottyLanguageServer] = implicitly[TestServer].server + +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/ActionOnMarker.scala b/language-server/test/dotty/tools/languageserver/util/actions/ActionOnMarker.scala new file mode 100644 index 000000000000..fca14d77b106 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/ActionOnMarker.scala @@ -0,0 +1,11 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.embedded.CodeMarker + +/** An action that defines a `marker` where the action will be performed. */ +trait ActionOnMarker extends Action { + + /** The marker that defines where the action will be performed. */ + def marker: CodeMarker + +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/ActionOnRange.scala b/language-server/test/dotty/tools/languageserver/util/actions/ActionOnRange.scala new file mode 100644 index 000000000000..6c345037142a --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/ActionOnRange.scala @@ -0,0 +1,33 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.{CodeRange, PositionContext} +import dotty.tools.languageserver.util.embedded.CodeMarker + +/** An action that executes on a range. */ +trait ActionOnRange extends Action { + + /** The range on which the action is performed. */ + def range: CodeRange + + /** The action to perform for every point of the range. */ + def onMarker(marker: CodeMarker): Exec[Unit] + + override def execute(): Exec[Unit] = { + val posCtx = implicitly[PositionContext] + range.check() + val file = range.file + val start = range.start + val startLine = start.line + val startCharacter = start.character + val endLine = range.end.line + val endCharacter = range.end.character + assert(startLine <= endLine, "Start of range should be before end " + range.show) + assert(startLine != endLine || startCharacter < endCharacter, "Start of range should be before end " + range.show) + assert(startLine == endLine, "multiline ranges not supported") // TODO implement multiline + (startCharacter until endCharacter).foreach { char => + val marker = new CodeMarker(start.name + "-with-offset=" + (char - startCharacter)) + implicit def posCtx2: PositionContext = posCtx.withPos(marker, (file, startLine, char)) + onMarker(marker) + } + } +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala new file mode 100644 index 000000000000..1c2c96699a26 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala @@ -0,0 +1,34 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.PositionContext +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.server.TestFile + +import org.eclipse.lsp4j.CompletionItemKind +import org.junit.Assert.{assertEquals, assertFalse, assertTrue} + +import scala.collection.JavaConverters._ + +/** + * An action requesting for code completion at `marker`, expecting `expected`. + * This action corresponds to the `textDocument/completion` method of the Language Server Protocol. + * + * @param marker The marker indicating the position where completion should be requested. + * @param expected The expected results from the language server. + */ +class CodeCompletion(override val marker: CodeMarker, + expected: Set[(String, CompletionItemKind, String)]) extends ActionOnMarker { + + override def execute(): Exec[Unit] = { + val result = server.completion(marker.toTextDocumentPositionParams).get() + assertTrue(s"Completion results were not 'right': $result", result.isRight) + assertFalse(s"Completion results were 'incomplete': $result", result.getRight.isIncomplete) + val completionResults = result.getRight.getItems.asScala.toSet.map { item => + (item.getLabel, item.getKind, item.getDetail) + } + assertEquals(expected, completionResults) + } + + override def show: PositionContext.PosCtx[String] = + s"CodeCompletion(${marker.show}, $expected)" +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeDefinition.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeDefinition.scala new file mode 100644 index 000000000000..0e1e41774ac2 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeDefinition.scala @@ -0,0 +1,28 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.{CodeRange, PositionContext} +import dotty.tools.languageserver.util.embedded.CodeMarker + +import scala.collection.JavaConverters._ + +import org.junit.Assert.assertEquals + +/** + * An action requesting for the definition of the symbol inside `range`. + * This action corresponds to the `textDocument/definition` method of the Language Server Protocol. + * + * @param range The range of positions for which to request the definition. + * @param expected The expected results. + */ +class CodeDefinition(override val range: CodeRange, expected: Seq[CodeRange]) extends ActionOnRange { + + override def onMarker(marker: CodeMarker): Exec[Unit] = { + val results = server.definition(marker.toTextDocumentPositionParams).get().asScala.toSeq + val expectedLocations = expected.map(_.toLocation) + + assertEquals(expectedLocations, results) + } + + override def show: PositionContext.PosCtx[String] = + s"CodeDefinition(${range.show}, ${expected.map(_.show)})" +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentHighlight.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentHighlight.scala new file mode 100644 index 000000000000..eefbac6dd40f --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentHighlight.scala @@ -0,0 +1,34 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.{CodeRange, PositionContext} +import org.eclipse.lsp4j.DocumentHighlightKind +import org.junit.Assert.assertEquals + +import scala.collection.JavaConverters._ + +/** + * An action requesting for the ranges that should be highlighted, when a position within `range` + * is selected. + * This action corresponds to the `textDocument/documentHighlight` method of the Language Server + * Protocol. + * + * @param range The range to of positions to test. + * @param expected The expected results. + */ +class CodeDocumentHighlight(override val range: CodeRange, + expected: Seq[(CodeRange, DocumentHighlightKind)]) extends ActionOnRange { + + override def onMarker(marker: CodeMarker): Exec[Unit] = { + val expectedPairs = expected.map { case (codeRange, kind) => (codeRange.toRange, kind) } + val results = server.documentHighlight(marker.toTextDocumentPositionParams).get() + val resultPairs = results.asScala.map { result => (result.getRange, result.getKind) } + + assertEquals(expectedPairs, resultPairs) + } + + override def show: PositionContext.PosCtx[String] = { + val (references, kinds) = expected.unzip + s"CodeDocumentHighlight(${range.show}, ${references.map(_.show)}, $kinds)" + } +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentSymbol.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentSymbol.scala new file mode 100644 index 000000000000..32d6bf5540f7 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeDocumentSymbol.scala @@ -0,0 +1,28 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.{PositionContext, SymInfo} +import org.junit.Assert.assertEquals + +import scala.collection.JavaConverters._ + +/** + * An action requesting for the symbols found in the document matching `marker`. + * This action corresponds to the `textDocument/documentSymbol` method of the Language Server + * Protocol. + * + * @param marker The marker that identifies the document for which to request the symbols. + * @param expected The expected symbols to receive. + */ +class CodeDocumentSymbol(override val marker: CodeMarker, expected: Seq[SymInfo]) extends ActionOnMarker { + + override def execute(): Exec[Unit] = { + val results = server.documentSymbol(marker.toDocumentSymbolParams).get().asScala + val expectedSymInfos = expected.map(_.toSymInformation) + + assertEquals(expectedSymInfos, results) + } + + override def show: PositionContext.PosCtx[String] = + s"CodeDocumentSymbol(${marker.show}, ${expected.map(_.show)})" +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeHover.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeHover.scala new file mode 100644 index 000000000000..62b1c8564b0f --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeHover.scala @@ -0,0 +1,31 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.{CodeRange, PositionContext} + +import org.junit.Assert.{assertEquals, assertNull, assertTrue} + +/** + * An action requesting for the info shown when `range` is hovered. + * This action corresponds to the `textDocument/hover` method of the Language Server Protocol. + * + * @param range The range of positions that should be hovered. + * @param expected The expected result. + */ +class CodeHover(override val range: CodeRange, expected: String) extends ActionOnRange { + + override def onMarker(marker: CodeMarker): Exec[Unit] = { + val result = server.hover(marker.toTextDocumentPositionParams).get() + assertNull(result.getRange) + if (expected.isEmpty) assertNull(result.getContents) + else { + assertEquals(1, result.getContents.size) + val content = result.getContents.get(0) + assertTrue(content.isLeft) + assertEquals(expected, content.getLeft) + } + } + + override def show: PositionContext.PosCtx[String] = + s"CodeHover(${range.show}, $expected)" +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeReferences.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeReferences.scala new file mode 100644 index 000000000000..758a2b0e43fc --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeReferences.scala @@ -0,0 +1,31 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.{CodeRange, PositionContext} + +import org.junit.Assert.assertEquals + +import scala.collection.JavaConverters._ + +/** + * An action requesting for all the references to the symbol in `range` within the workspace. + * This action corresponds to the `textDocument/references` method of the Language Server Protocol. + * + * @param range The range of positions to test. + * @param expected The expected results. + * @param withDecl Whether the declaration of the current symbol should be included. + */ +class CodeReferences(override val range: CodeRange, + expected: List[CodeRange], + withDecl: Boolean) extends ActionOnRange { + + override def onMarker(marker: CodeMarker): Exec[Unit] = { + val expectedLocations = expected.map(_.toLocation) + val results = server.references(marker.toReferenceParams(withDecl)).get().asScala + + assertEquals(expectedLocations, results) + } + + override def show: PositionContext.PosCtx[String] = + s"CodeReferences(${range.show}, ${expected.map(_.show)}, $withDecl)" +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeRename.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeRename.scala new file mode 100644 index 000000000000..5e69d00b4bbb --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeRename.scala @@ -0,0 +1,33 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.{CodeRange, PositionContext} + +import org.junit.Assert.{assertEquals, assertNull} + +import scala.collection.JavaConverters._ + +/** + * An action requesting for a rename of the symbol at `marker`. + * This action corresponds to the `textDocument/rename` method of the Language Server Protocol. + * + * @param marker The positions where to test to rename. + * @param newName The new name to give to the selected symbol. + * @param expected The expected ranges that should be modified. + */ +class CodeRename(override val marker: CodeMarker, + newName: String, + expected: Set[CodeRange]) extends ActionOnMarker { + + override def execute(): Exec[Unit] = { + val results = server.rename(marker.toRenameParams(newName)).get() + val changes = results.getChanges.asScala.mapValues(_.asScala.toSet.map(ch => (ch.getNewText, ch.getRange))) + val expectedChanges = expected.groupBy(_.file.uri).mapValues(_.map(range => (newName, range.toRange))) + + assertNull(results.getDocumentChanges) + assertEquals(expectedChanges, changes) + } + + override def show: PositionContext.PosCtx[String] = + s"CodeRename(${marker.show}, $newName, ${expected.map(_.show)})" +} diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeSymbol.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeSymbol.scala new file mode 100644 index 000000000000..3fd5d59b15b4 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeSymbol.scala @@ -0,0 +1,27 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.{PositionContext, SymInfo} +import org.eclipse.lsp4j.WorkspaceSymbolParams +import org.junit.Assert.assertEquals + +import scala.collection.JavaConverters._ + +/** + * An action requesting for all the symbols in the workspace matching `query`. + * This action corresponds to the `workspace/symbol` method of the Language Server Protocol. + * + * @param query The string to query for. + * @param expected The expected results. + */ +class CodeSymbol(query: String, expected: Seq[SymInfo]) extends Action { + + override def execute(): Exec[Unit] = { + val results = server.symbol(new WorkspaceSymbolParams(query)).get().asScala + val expectedSymInfo = expected.map(_.toSymInformation) + + assertEquals(expectedSymInfo, results) + } + + override def show: PositionContext.PosCtx[String] = + s"CodeDocumentSymbol($query, ${expected.map(_.show)})" +} diff --git a/language-server/test/dotty/tools/languageserver/util/embedded/CodeInRange.scala b/language-server/test/dotty/tools/languageserver/util/embedded/CodeInRange.scala new file mode 100644 index 000000000000..ac8724346316 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/embedded/CodeInRange.scala @@ -0,0 +1,11 @@ +package dotty.tools.languageserver.util.embedded + +import dotty.tools.languageserver.util.CodeRange + +/** + * Wrapper for some text that appears within a range. + * + * @param text The text that is comprised inside `range`. + * @param range The range of positions that encloses the `text`. + */ +case class CodeInRange(text: String, range: CodeRange) extends Embedded diff --git a/language-server/test/dotty/tools/languageserver/util/embedded/CodeMarker.scala b/language-server/test/dotty/tools/languageserver/util/embedded/CodeMarker.scala new file mode 100644 index 000000000000..5f7416927474 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/embedded/CodeMarker.scala @@ -0,0 +1,51 @@ +package dotty.tools.languageserver.util.embedded + +import dotty.tools.languageserver.util.server.TestFile +import dotty.tools.languageserver.util.{CodeRange, PositionContext} + +import org.eclipse.lsp4j._ + +import PositionContext.PosCtx + +/** Used to mark positions in the code */ +class CodeMarker(val name: String) extends Embedded { + + /** A range of positions between this marker and `other`. */ + def to(other: CodeMarker): CodeRange = CodeRange(this, other) + + /** The file containing this marker. */ + def file: PosCtx[TestFile] = posCtx.positionOf(this)._1 + + /** The line containing this marker. */ + def line: PosCtx[Int] = posCtx.positionOf(this)._2 + + /** The columng number of this marker. */ + def character: PosCtx[Int] = posCtx.positionOf(this)._3 + + /** Converts this marker to a position. */ + def toPosition: PosCtx[Position] = new Position(line, character) + + def toTextDocumentPositionParams: PosCtx[TextDocumentPositionParams] = + new TextDocumentPositionParams(toTextDocumentIdentifier, toPosition) + + def toDocumentSymbolParams: PosCtx[DocumentSymbolParams] = + new DocumentSymbolParams(toTextDocumentIdentifier) + + def toRenameParams(newName: String): PosCtx[RenameParams] = + new RenameParams(toTextDocumentIdentifier, toPosition, newName) + + def toTextDocumentIdentifier: PosCtx[TextDocumentIdentifier] = + new TextDocumentIdentifier(file.uri) + + def toReferenceParams(withDecl: Boolean): PosCtx[ReferenceParams] = { + val rp = new ReferenceParams(new ReferenceContext(withDecl)) + rp.setTextDocument(toTextDocumentIdentifier) + rp.setPosition(toPosition) + rp + } + + def show: PosCtx[String] = s"($name,line=$line,char=$character)" + override def toString: String = s"CodePosition($name)" + + private implicit def posCtx(implicit ctx: PositionContext): PositionContext = ctx +} diff --git a/language-server/test/dotty/tools/languageserver/util/embedded/Embedded.scala b/language-server/test/dotty/tools/languageserver/util/embedded/Embedded.scala new file mode 100644 index 000000000000..ce96f6bf5d1e --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/embedded/Embedded.scala @@ -0,0 +1,4 @@ +package dotty.tools.languageserver.util.embedded + +/** Base trait for objects that can be used inside the `code""` interpolator. */ +trait Embedded diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala b/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala new file mode 100644 index 000000000000..c900aadf05f0 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/server/TestClient.scala @@ -0,0 +1,35 @@ +package dotty.tools.languageserver.util.server + +import java.util.concurrent.CompletableFuture + +import org.eclipse.lsp4j._ +import org.eclipse.lsp4j.services._ + +class TestClient extends LanguageClient { + + private val log = new StringBuilder + + def getLog: String = log.result() + + override def logMessage(message: MessageParams) = { + log.append(message.toString) + } + + override def showMessage(messageParams: MessageParams) = { + log.append(messageParams.toString) + } + + override def telemetryEvent(obj: scala.Any) = { + log.append(obj.toString) + } + + override def showMessageRequest(requestParams: ShowMessageRequestParams) = { + log.append(requestParams.toString) + new CompletableFuture[MessageActionItem] + } + + override def publishDiagnostics(diagnostics: PublishDiagnosticsParams) = { + log.append(diagnostics.toString) + } + +} diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestFile.scala b/language-server/test/dotty/tools/languageserver/util/server/TestFile.scala new file mode 100644 index 000000000000..a77e82dbdc40 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/server/TestFile.scala @@ -0,0 +1,14 @@ +package dotty.tools.languageserver.util.server + +import java.nio.file.{Path, Paths} + +import org.eclipse.lsp4j.TextDocumentIdentifier + +class TestFile(val file: String) extends AnyVal { + def uri: String = s"file://${TestFile.sourceDir}/$file" +} + +object TestFile { + lazy val testDir: Path = Paths.get("../out/ide-tests").toAbsolutePath + lazy val sourceDir: Path = testDir.resolve("src") +} diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala new file mode 100644 index 000000000000..39c53ccfb0c9 --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala @@ -0,0 +1,63 @@ +package dotty.tools.languageserver.util.server + +import java.io.PrintWriter +import java.net.URI +import java.nio.file.Path +import java.util + +import dotty.tools.languageserver.DottyLanguageServer +import org.eclipse.lsp4j.{ DidOpenTextDocumentParams, InitializeParams, InitializeResult, TextDocumentItem} + +class TestServer(testFolder: Path) { + + val server = new DottyLanguageServer + init() + + private[this] def init(): InitializeResult = { + // Fill the configuration with values populated by sbt + def showSeq[T](lst: Seq[T]): String = lst.map(elem => '"' + elem.toString + '"').mkString("[ ", ", ", " ]") + val dottyIdeJson: String = + s"""[ { + | "id" : "dotty-ide-test", + | "compilerVersion" : "${BuildInfo.ideTestsCompilerVersion}", + | "compilerArguments" : ${showSeq(BuildInfo.ideTestsCompilerArguments)}, + | "sourceDirectories" : ${showSeq(BuildInfo.ideTestsSourceDirectories)}, + | "dependencyClasspath" : ${showSeq(BuildInfo.ideTestsDependencyClasspath)}, + | "classDirectory" : "${BuildInfo.ideTestsClassDirectory}" + |} + |]""".stripMargin + val configFile = testFolder.resolve(DottyLanguageServer.IDE_CONFIG_FILE) + testFolder.toFile.mkdirs() + testFolder.resolve("src").toFile.mkdirs() + testFolder.resolve("out").toFile.mkdirs() + + new PrintWriter(configFile.toString) { + write(dottyIdeJson) + close() + } + + val client = new TestClient + server.connect(client) + + val initParams = new InitializeParams() + initParams.setRootUri(testFolder.toAbsolutePath.toUri.toString) + server.initialize(initParams).get() + } + + /** Open the code in the given file and returns the file. + * @param code code in file + * @param fileName file path in the source directory + * @return the file opened + */ + def openCode(code: String, fileName: String): TestFile = { + val testFile = new TestFile(fileName) + val dotdp = new DidOpenTextDocumentParams() + val tdi = new TextDocumentItem() + tdi.setUri(testFile.uri) + tdi.setText(code) + dotdp.setTextDocument(tdi) + server.didOpen(dotdp) + testFile + } + +} diff --git a/project/Build.scala b/project/Build.scala index e93cbb946b88..eaca19bdc07a 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -18,6 +18,9 @@ import dotty.tools.sbtplugin.DottyPlugin.autoImport._ import dotty.tools.sbtplugin.DottyIDEPlugin.{ prepareCommand, runProcess } import dotty.tools.sbtplugin.DottyIDEPlugin.autoImport._ +import sbtbuildinfo.BuildInfoPlugin +import sbtbuildinfo.BuildInfoPlugin.autoImport._ + /* In sbt 0.13 the Build trait would expose all vals to the shell, where you * can use them in "set a := b" like expressions. This re-exposes them. */ @@ -85,6 +88,13 @@ object Build { // Only available in vscode-dotty lazy val unpublish = taskKey[Unit]("Unpublish a package") + // Settings used to configure the test language server + lazy val ideTestsCompilerVersion = taskKey[String]("Compiler version to use in IDE tests") + lazy val ideTestsCompilerArguments = taskKey[Seq[String]]("Compiler arguments to use in IDE tests") + lazy val ideTestsSourceDirectories = taskKey[Seq[File]]("Source directories to use in IDE tests") + lazy val ideTestsDependencyClasspath = taskKey[Seq[File]]("Dependency classpath to use in IDE tests") + lazy val ideTestsClassDirectory = taskKey[File]("Class directory to use in IDE tests") + // Settings shared by the build (scoped in ThisBuild). Used in build.sbt lazy val thisBuildSettings = Def.settings( organization := dottyOrganization, @@ -766,6 +776,7 @@ object Build { // fork so that the shutdown hook in Main is run when we ctrl+c a run // (you need to have `cancelable in Global := true` in your global sbt config to ctrl+c a run) fork in run := true, + fork in Test := true, libraryDependencies ++= Seq( "org.eclipse.lsp4j" % "org.eclipse.lsp4j" % "0.3.0", Dependencies.`jackson-databind` @@ -788,6 +799,32 @@ object Build { runTask(Runtime, mainClass, allArgs: _*) }.dependsOn(compile in (`vscode-dotty`, Compile)).evaluated + ). + settings( + ideTestsCompilerVersion := (version in `dotty-compiler`).value, + ideTestsCompilerArguments := (scalacOptions in `dotty-compiler`).value, + ideTestsSourceDirectories := Seq((baseDirectory in ThisBuild).value / "out" / "ide-tests" / "src"), + ideTestsDependencyClasspath := { + val dottyLib = (classDirectory in `dotty-library-bootstrapped` in Compile).value + val scalaLib = + (dependencyClasspath in `dotty-library-bootstrapped` in Compile) + .value + .map(_.data) + .filter(_.getName.matches("scala-library.*\\.jar")) + .toList + dottyLib :: scalaLib + }, + ideTestsClassDirectory := (baseDirectory in ThisBuild).value / "out" / "ide-tests" / "out", + buildInfoKeys in Test := Seq[BuildInfoKey]( + ideTestsCompilerVersion, + ideTestsCompilerArguments, + ideTestsSourceDirectories, + ideTestsDependencyClasspath, + ideTestsClassDirectory + ), + buildInfoPackage in Test := "dotty.tools.languageserver.util.server", + BuildInfoPlugin.buildInfoScopedSettings(Test), + BuildInfoPlugin.buildInfoDefaultSettings ).disablePlugins(ScriptedPlugin) lazy val `dotty-bench` = project.in(file("bench")).asDottyBench(NonBootstrapped) diff --git a/project/plugins.sbt b/project/plugins.sbt index 8e9587ae889d..83690d191839 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -14,3 +14,5 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.10.1") addSbtPlugin("pl.project13.scala" % "sbt-jmh" % "0.3.2") + +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.7.0")