From 9ef88ae1a9c15341fefe53fb5dbafad148f45c56 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 29 Aug 2018 16:13:23 +0200 Subject: [PATCH 1/7] Support multi-project setups in the IDE tests --- .../tools/languageserver/util/Code.scala | 48 ++++++++++++- .../languageserver/util/CodeTester.scala | 22 ++++-- .../util/server/TestServer.scala | 68 +++++++++++++------ 3 files changed, 108 insertions(+), 30 deletions(-) diff --git a/language-server/test/dotty/tools/languageserver/util/Code.scala b/language-server/test/dotty/tools/languageserver/util/Code.scala index 9fe5009d5422..396e5b9fa699 100644 --- a/language-server/test/dotty/tools/languageserver/util/Code.scala +++ b/language-server/test/dotty/tools/languageserver/util/Code.scala @@ -99,8 +99,11 @@ object Code { } } - /** A new `CodeTester` working with `sources` in the workspace. */ - def withSources(sources: SourceWithPositions*): CodeTester = new CodeTester(sources.toList, Nil) + /** A new `CodeTester` working with a single workspace containing `sources`. */ + def withSources(sources: SourceWithPositions*): CodeTester = withWorkspaces(Workspace(sources.toList)) + + /** A new `CodeTester` working with `workspaces`. */ + def withWorkspaces(workspaces: Workspace*): CodeTester = new CodeTester(workspaces.toList) sealed trait SourceWithPositions { @@ -111,7 +114,7 @@ object Code { def positions: List[(CodeMarker, Int, Int)] /** A new `CodeTester` with only this source in the workspace. */ - def withSource: CodeTester = new CodeTester(this :: Nil, Nil) + def withSource: CodeTester = withSources(this) } @@ -131,4 +134,43 @@ object Code { */ case class WorksheetWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions + /** + * A group of sources belonging to the same project. + * + * @param sources The sources that this workspace holds. + * @param name The name of this workspace + * @param dependsOn The other workspaces on which this workspace depend. + */ + case class Workspace(sources: List[SourceWithPositions], + name: String = Workspace.freshName, + dependsOn: List[Workspace] = Nil) { + + /** + * Add `sources` to the sources of this workspace. + */ + def withSources(sources: SourceWithPositions*): Workspace = copy(sources = this.sources ::: sources.toList) + + } + + object Workspace { + private[this] val count = new java.util.concurrent.atomic.AtomicInteger() + private def freshName: String = s"workspace${count.incrementAndGet()}" + + /** + * Creates a new workspace that depends on `workspaces`. + * + * @param workspaces The dependencies of the new workspace. + * @return An empty workspace with a dependency on the specified workspaces. + */ + def dependingOn(workspaces: Workspace*) = new Workspace(Nil, dependsOn = workspaces.toList) + + /** + * Create a new workspace with the given sources. + * + * @param sources The sources to add to this workspace. + * @return a new workspace containing the specified sources. + */ + def withSources(sources: SourceWithPositions*): Workspace = new Workspace(sources.toList) + } + } diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala index b9c91a0853d0..b5d13e47dbae 100644 --- a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -10,16 +10,18 @@ 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]) { +class CodeTester(workspaces: List[Workspace]) { - private val testServer = new TestServer(TestFile.testDir) + private val testServer = new TestServer(TestFile.testDir, workspaces) + private val sources = for { workspace <- workspaces + source <- workspace.sources } yield (workspace, source) private val files = sources.zipWithIndex.map { - case (ScalaSourceWithPositions(text, _), i) => testServer.openCode(text, s"Source$i.scala") - case (WorksheetWithPositions(text, _), i) => testServer.openCode(text, s"Worksheet$i.sc") + case ((workspace, ScalaSourceWithPositions(text, _)), i) => testServer.openCode(text, workspace, s"Source$i.scala") + case ((workspace, WorksheetWithPositions(text, _)), i) => testServer.openCode(text, workspace, s"Worksheet$i.sc") } + private val positions: PositionContext = getPositions(files) /** @@ -158,7 +160,13 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) { action.execute()(testServer, testServer.client, positions) } catch { case ex: AssertionError => - val sourcesStr = sources.zip(files).map{ case (source, file) => "// " + file.file + "\n" + source.text}.mkString("\n") + val sourcesStr = + sources.zip(files).map { + case ((workspace, source), file) => + s"""// ${file.file} in workspace ${workspace.name} + |${source.text}""".stripMargin + }.mkString(System.lineSeparator) + val msg = s""" | @@ -177,7 +185,7 @@ class CodeTester(sources: List[SourceWithPositions], actions: List[Action]) { private def getPositions(files: List[TestFile]): PositionContext = { val posSeq = { for { - (code, file) <- sources.zip(files) + ((_, code), file) <- sources.zip(files) (position, line, char) <- code.positions } yield position -> (file, line, char) } diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala index 6859b9e2d777..bd744848e22f 100644 --- a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala +++ b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala @@ -1,14 +1,16 @@ package dotty.tools.languageserver.util.server import java.io.PrintWriter +import java.io.File.{separator => sep} import java.net.URI -import java.nio.file.Path +import java.nio.file.{Files, Path} import java.util import dotty.tools.languageserver.DottyLanguageServer +import dotty.tools.languageserver.util.Code.Workspace import org.eclipse.lsp4j.{ DidOpenTextDocumentParams, InitializeParams, InitializeResult, TextDocumentItem} -class TestServer(testFolder: Path) { +class TestServer(testFolder: Path, workspaces: List[Workspace]) { val server = new DottyLanguageServer var client: TestClient = _ @@ -16,28 +18,54 @@ class TestServer(testFolder: Path) { 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.replace('\\', '/') + '"') - .mkString("[ ", ", ", " ]") - val dottyIdeJson: String = - s"""[ { - | "id" : "dotty-ide-test", + /** + * Set up given workspace, return JSON config. + * + * This creates the necessary directories to hold the classes and sources. Some values + * are passed via sbt-buildinfo, such as the classpath containing the scala and dotty libaries. + * + * @param workspace The workspace to configure. + * @return A JSON object representing the configuration for this workspace. + */ + def workspaceSetup(workspace: Workspace): String = { + def showSeq[T](lst: Seq[T]): String = + lst + .map(elem => '"' + elem.toString.replace('\\', '/') + '"') + .mkString("[ ", ", ", " ]") + + def classDirectory(workspace: Workspace): Path = { + val path = testFolder.resolve(workspace.name).resolve("out") + Files.createDirectories(path) + path.toAbsolutePath + } + + val dependencyClasspath = + BuildInfo.ideTestsDependencyClasspath.map(_.getAbsolutePath) ++ + workspace.dependsOn.map(w => classDirectory(w).toString) + + val sourceDirectory: Path = { + val path = TestFile.sourceDir.resolve(workspace.name).toAbsolutePath + Files.createDirectories(path) + path + } + + s"""{ + | "id" : "${workspace.name}", | "compilerVersion" : "${BuildInfo.ideTestsCompilerVersion}", | "compilerArguments" : ${showSeq(BuildInfo.ideTestsCompilerArguments)}, - | "sourceDirectories" : ${showSeq(BuildInfo.ideTestsSourceDirectories)}, - | "dependencyClasspath" : ${showSeq(BuildInfo.ideTestsDependencyClasspath)}, - | "classDirectory" : "${BuildInfo.ideTestsClassDirectory.toString.replace('\\','/')}" + | "sourceDirectories" : ${showSeq(sourceDirectory :: Nil)}, + | "dependencyClasspath" : ${showSeq(dependencyClasspath)}, + | "classDirectory" : "${classDirectory(workspace).toString.replace('\\','/')}" |} - |]""".stripMargin + |""".stripMargin + } + + Files.createDirectories(testFolder) val configFile = testFolder.resolve(DottyLanguageServer.IDE_CONFIG_FILE) - testFolder.toFile.mkdirs() - testFolder.resolve("src").toFile.mkdirs() - testFolder.resolve("out").toFile.mkdirs() + val configuration = workspaces.map(workspaceSetup).mkString("[", ",", "]") new PrintWriter(configFile.toString) { - write(dottyIdeJson) + write(configuration) close() } @@ -54,8 +82,8 @@ class TestServer(testFolder: Path) { * @param fileName file path in the source directory * @return the file opened */ - def openCode(code: String, fileName: String): TestFile = { - val testFile = new TestFile(fileName) + def openCode(code: String, workspace: Workspace, fileName: String): TestFile = { + val testFile = new TestFile(workspace.name + sep + fileName) val dotdp = new DidOpenTextDocumentParams() val tdi = new TextDocumentItem() tdi.setUri(testFile.uri) From 9c6a8b64a722471edfe7f74b0d97fe88c04eff98 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Wed, 29 Aug 2018 16:21:49 +0200 Subject: [PATCH 2/7] Remove unused settings in Build.scala --- project/Build.scala | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/project/Build.scala b/project/Build.scala index e5ddd91d0de4..807599da1f3f 100644 --- a/project/Build.scala +++ b/project/Build.scala @@ -103,9 +103,7 @@ object Build { // 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( @@ -883,7 +881,6 @@ object Build { 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 = @@ -894,13 +891,10 @@ object Build { .toList dottyLib :: scalaLib }, - ideTestsClassDirectory := (baseDirectory in ThisBuild).value / "out" / "ide-tests" / "out", buildInfoKeys in Test := Seq[BuildInfoKey]( ideTestsCompilerVersion, ideTestsCompilerArguments, - ideTestsSourceDirectories, - ideTestsDependencyClasspath, - ideTestsClassDirectory + ideTestsDependencyClasspath ), buildInfoPackage in Test := "dotty.tools.languageserver.util.server", BuildInfoPlugin.buildInfoScopedSettings(Test), From d51e463a6c0c1a6fd224d4e51edae1f646a25aa0 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Thu, 30 Aug 2018 10:27:07 +0200 Subject: [PATCH 3/7] IDE tests: compile dependent workspaces When writing IDE tests that simulate multi-project setups, we need to compile the sources of the workspaces that are depended on, because the dependent workspaces require their class files. This commit changes the `TestServer` so that workspaces that are depended on are compiled during the initialization of the server. When there are no dependency relation between workspaces, no compilation is performed. --- .../languageserver/util/CodeTester.scala | 11 ++- .../util/server/TestServer.scala | 90 ++++++++++++++----- 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala index b5d13e47dbae..7319baa4e297 100644 --- a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -17,10 +17,13 @@ class CodeTester(workspaces: List[Workspace]) { private val sources = for { workspace <- workspaces source <- workspace.sources } yield (workspace, source) - private val files = sources.zipWithIndex.map { - case ((workspace, ScalaSourceWithPositions(text, _)), i) => testServer.openCode(text, workspace, s"Source$i.scala") - case ((workspace, WorksheetWithPositions(text, _)), i) => testServer.openCode(text, workspace, s"Worksheet$i.sc") - } + + private val files = + for { workspace <- workspaces + (source, id) <- workspace.sources.zipWithIndex } yield source match { + case ScalaSourceWithPositions(text, _) => testServer.openCode(text, workspace, s"Source$id.scala") + case WorksheetWithPositions(text, _) => testServer.openCode(text, workspace, s"Worksheet$id.sc") + } private val positions: PositionContext = getPositions(files) diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala index bd744848e22f..a103557a9b20 100644 --- a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala +++ b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala @@ -1,11 +1,14 @@ package dotty.tools.languageserver.util.server import java.io.PrintWriter -import java.io.File.{separator => sep} +import java.io.File.{pathSeparator, separator} import java.net.URI import java.nio.file.{Files, Path} import java.util +import dotty.tools.dotc.Main +import dotty.tools.dotc.reporting.{Reporter, ThrowingReporter} +import dotty.tools.io.Directory import dotty.tools.languageserver.DottyLanguageServer import dotty.tools.languageserver.util.Code.Workspace import org.eclipse.lsp4j.{ DidOpenTextDocumentParams, InitializeParams, InitializeResult, TextDocumentItem} @@ -18,11 +21,21 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { init() private[this] def init(): InitializeResult = { + var compiledWorkspaces: Set[Workspace] = Set.empty + + /** Compile the dependencies of the given workspace, and then the workspace. */ + def compileWorkspaceAndDependencies(workspace: Workspace): Unit = + if (!compiledWorkspaces.contains(workspace)) { + workspace.dependsOn.foreach(compileWorkspaceAndDependencies) + compileWorkspace(workspace) + compiledWorkspaces += workspace + } + /** * Set up given workspace, return JSON config. * - * This creates the necessary directories to hold the classes and sources. Some values - * are passed via sbt-buildinfo, such as the classpath containing the scala and dotty libaries. + * If the workspace has dependencies, these dependencies are compiled. The classfiles of the + * dependent workspaces are put on the classpath of this workspace. * * @param workspace The workspace to configure. * @return A JSON object representing the configuration for this workspace. @@ -33,29 +46,16 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { .map(elem => '"' + elem.toString.replace('\\', '/') + '"') .mkString("[ ", ", ", " ]") - def classDirectory(workspace: Workspace): Path = { - val path = testFolder.resolve(workspace.name).resolve("out") - Files.createDirectories(path) - path.toAbsolutePath - } - - val dependencyClasspath = - BuildInfo.ideTestsDependencyClasspath.map(_.getAbsolutePath) ++ - workspace.dependsOn.map(w => classDirectory(w).toString) - - val sourceDirectory: Path = { - val path = TestFile.sourceDir.resolve(workspace.name).toAbsolutePath - Files.createDirectories(path) - path - } + // Compile all the dependencies of this workspace + workspace.dependsOn.foreach(compileWorkspaceAndDependencies) s"""{ | "id" : "${workspace.name}", | "compilerVersion" : "${BuildInfo.ideTestsCompilerVersion}", | "compilerArguments" : ${showSeq(BuildInfo.ideTestsCompilerArguments)}, - | "sourceDirectories" : ${showSeq(sourceDirectory :: Nil)}, - | "dependencyClasspath" : ${showSeq(dependencyClasspath)}, - | "classDirectory" : "${classDirectory(workspace).toString.replace('\\','/')}" + | "sourceDirectories" : ${showSeq(sourceDirectory(workspace, wipe = false) :: Nil)}, + | "dependencyClasspath" : ${showSeq(dependencyClasspath(workspace))}, + | "classDirectory" : "${classDirectory(workspace, wipe = false).toString.replace('\\','/')}" |} |""".stripMargin } @@ -83,7 +83,7 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { * @return the file opened */ def openCode(code: String, workspace: Workspace, fileName: String): TestFile = { - val testFile = new TestFile(workspace.name + sep + fileName) + val testFile = new TestFile(workspace.name + separator + fileName) val dotdp = new DidOpenTextDocumentParams() val tdi = new TextDocumentItem() tdi.setUri(testFile.uri) @@ -93,4 +93,50 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { testFile } + private def classDirectory(workspace: Workspace, wipe: Boolean): Path = { + val path = testFolder.resolve(workspace.name).resolve("out") + if (wipe) { + Directory(path).deleteRecursively() + Files.createDirectories(path) + } + path.toAbsolutePath + } + + private def dependencyClasspath(workspace: Workspace) = + BuildInfo.ideTestsDependencyClasspath.map(_.getAbsolutePath) ++ + workspace.dependsOn.map(w => classDirectory(w, wipe = false).toString) + + private def sourceDirectory(workspace: Workspace, wipe: Boolean): Path = { + val path = TestFile.sourceDir.resolve(workspace.name).toAbsolutePath + if (wipe) { + Directory(path).deleteRecursively() + Files.createDirectories(path) + } + path + } + + /** + * Sets up the sources of the given workspace, creates the necessary directories + * and compile the sources. + * + * @param workspace The workspace to set up. + */ + private def compileWorkspace(workspace: Workspace): Unit = { + val sourcesDir = sourceDirectory(workspace, wipe = true) + val sources = workspace.sources.zipWithIndex.map { case (src, id) => + val path = sourcesDir.resolve(s"Source${id}.scala").toAbsolutePath + Files.write(path, src.text.getBytes("UTF-8")) + path.toString + } + + val compileOptions = + sources.toArray ++ + Array( + "-classpath", dependencyClasspath(workspace).mkString(pathSeparator), + "-d", classDirectory(workspace, wipe = true).toString + ) + val reporter = new ThrowingReporter(Reporter.NoReporter) + Main.process(compileOptions, reporter) + } + } From fdedd8af5e83f8efff9650aaa6dc4d8cc67fe328 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Thu, 30 Aug 2018 10:34:48 +0200 Subject: [PATCH 4/7] Add first multi-workspace IDE test --- .../tools/languageserver/DefinitionTest.scala | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala index 6c11d6d68786..de1590dc7a22 100644 --- a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala +++ b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala @@ -37,6 +37,20 @@ class DefinitionTest { .definition(m13 to m14, List(m3 to m4)) } + @Test def classDefinitionDifferentWorkspace: Unit = { + val w0 = Workspace.withSources( + code"""class ${m1}Foo${m2}""" + ) + + val w1 = Workspace.dependingOn(w0).withSources( + code"""class Bar { new ${m3}Foo${m4} }""" + ) + + withWorkspaces(w0, w1) + .definition(m1 to m2, List(m1 to m2)) + .definition(m3 to m4, List(m1 to m2)) + } + @Test def valDefinition0: Unit = { withSources( code"class Foo { val ${m1}x$m2 = 0; ${m3}x$m4 }", From 93fcd94a21a024c9dd030f003a3dd0a3520851b5 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Tue, 4 Sep 2018 11:32:55 +0200 Subject: [PATCH 5/7] Fix TestServer to include transitive dependencies Test workspaces should be created with the complete dependency classpath of their dependencies. The classpath was wrongly built with only the class directory of their dependencies. --- .../test/dotty/tools/languageserver/DefinitionTest.scala | 7 ++++++- .../tools/languageserver/util/server/TestServer.scala | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala index de1590dc7a22..81234d62984f 100644 --- a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala +++ b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala @@ -46,9 +46,14 @@ class DefinitionTest { code"""class Bar { new ${m3}Foo${m4} }""" ) - withWorkspaces(w0, w1) + val w2 = Workspace.dependingOn(w1).withSources( + code"""class Baz extends ${m5}Foo${m6}""" + ) + + withWorkspaces(w0, w1, w2) .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 = { diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala index a103557a9b20..f400196b5042 100644 --- a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala +++ b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala @@ -102,9 +102,12 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { path.toAbsolutePath } - private def dependencyClasspath(workspace: Workspace) = + private def dependencyClasspath(workspace: Workspace): Seq[String] = { BuildInfo.ideTestsDependencyClasspath.map(_.getAbsolutePath) ++ - workspace.dependsOn.map(w => classDirectory(w, wipe = false).toString) + workspace.dependsOn.flatMap { dep => + classDirectory(dep, wipe = false).toString +: dependencyClasspath(dep) + } + }.distinct private def sourceDirectory(workspace: Workspace, wipe: Boolean): Path = { val path = TestFile.sourceDir.resolve(workspace.name).toAbsolutePath From 67516161b43e0bdecd409bb7c9175711d4c6bbd4 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Mon, 15 Oct 2018 11:48:53 +0200 Subject: [PATCH 6/7] Fix #5223: Add `tasty` interpolator in IDE tests This interpolator is used to defined virtual source files that will be compiled before the test starts running. Unlike virtual source files defined with `code`, the ones defined with `tasty` will not be opened in the IDE (`textDocument/didOpen` will not be sent to the LSP server). As a result, the language server will unpickle the trees from TASTY. Fixes #5223 --- .../tools/languageserver/DefinitionTest.scala | 11 ++++++ .../tools/languageserver/util/Code.scala | 34 +++++++++++++++++-- .../languageserver/util/CodeTester.scala | 4 +-- .../util/server/TestServer.scala | 25 +++++++++----- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala index 81234d62984f..d63c20831fea 100644 --- a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala +++ b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala @@ -208,4 +208,15 @@ class DefinitionTest { .definition(m9 to m10, List(m3 to m4)) } + @Test def definitionFromTasty: Unit = { + withSources( + tasty"""package mypackage + class ${m1}A${m2}""", + code"""package mypackage + object O { + new ${m3}A${m4} + }""" + ).definition(m3 to m4, List(m1 to m2)) + } + } diff --git a/language-server/test/dotty/tools/languageserver/util/Code.scala b/language-server/test/dotty/tools/languageserver/util/Code.scala index 396e5b9fa699..5594e9c914fb 100644 --- a/language-server/test/dotty/tools/languageserver/util/Code.scala +++ b/language-server/test/dotty/tools/languageserver/util/Code.scala @@ -60,6 +60,17 @@ object Code { WorksheetWithPositions(text, positions) } + /** + * An interpolator similar to `code`, but used for defining a source that will + * be unpickled from TASTY. + * + * @see code + */ + def tasty(args: Embedded*): TastyWithPositions = { + val (text, positions) = textAndPositions(args: _*) + TastyWithPositions(text, positions) + } + private def textAndPositions(args: Embedded*): (String, List[(CodeMarker, Int, Int)]) = { val pi = sc.parts.iterator val ai = args.iterator @@ -107,7 +118,10 @@ object Code { sealed trait SourceWithPositions { - /** The code contained within the virtual source file. */ + /** A name for this source given its index. */ + def sourceName(index: Int): String + + /** The code contained within the virtual source file. */ def text: String /** The positions of the markers that have been set. */ @@ -124,7 +138,9 @@ object Code { * @param text The code contained within the virtual source file. * @param positions The positions of the markers that have been set. */ - case class ScalaSourceWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions + case class ScalaSourceWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions { + def sourceName(index: Int): String = s"Source$index.scala" + } /** * A virtual worksheet where several markers have been set. @@ -132,7 +148,19 @@ object Code { * @param text The code contained within the virtual source file. * @param positions The positions of the markers that have been set. */ - case class WorksheetWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions + case class WorksheetWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions { + def sourceName(index: Int): String = s"Worksheet$index.sc" + } + + /** + * A virtual source file that will not be opened in the IDE, but instead unpickled from TASTY. + * + * @param text The code contained within the virtual source file. + * @param positions The positions of the markers that have been set. + */ + case class TastyWithPositions(text: String, positions: List[(CodeMarker, Int, Int)]) extends SourceWithPositions { + def sourceName(index: Int): String = s"Source-from-tasty-$index.scala" + } /** * A group of sources belonging to the same project. diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala index 7319baa4e297..7fef3161bad0 100644 --- a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -21,8 +21,8 @@ class CodeTester(workspaces: List[Workspace]) { private val files = for { workspace <- workspaces (source, id) <- workspace.sources.zipWithIndex } yield source match { - case ScalaSourceWithPositions(text, _) => testServer.openCode(text, workspace, s"Source$id.scala") - case WorksheetWithPositions(text, _) => testServer.openCode(text, workspace, s"Worksheet$id.sc") + case src @ TastyWithPositions(text, _) => testServer.openCode(text, workspace, src.sourceName(id), openInIDE = false) + case other => testServer.openCode(other.text, workspace, other.sourceName(id), openInIDE = true) } private val positions: PositionContext = getPositions(files) diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala index f400196b5042..0996a92585bc 100644 --- a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala +++ b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala @@ -10,7 +10,7 @@ import dotty.tools.dotc.Main import dotty.tools.dotc.reporting.{Reporter, ThrowingReporter} import dotty.tools.io.Directory import dotty.tools.languageserver.DottyLanguageServer -import dotty.tools.languageserver.util.Code.Workspace +import dotty.tools.languageserver.util.Code.{TastyWithPositions, Workspace} import org.eclipse.lsp4j.{ DidOpenTextDocumentParams, InitializeParams, InitializeResult, TextDocumentItem} class TestServer(testFolder: Path, workspaces: List[Workspace]) { @@ -46,8 +46,12 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { .map(elem => '"' + elem.toString.replace('\\', '/') + '"') .mkString("[ ", ", ", " ]") - // Compile all the dependencies of this workspace - workspace.dependsOn.foreach(compileWorkspaceAndDependencies) + if (workspace.sources.exists(_.isInstanceOf[TastyWithPositions])) { + compileWorkspaceAndDependencies(workspace) + } else { + // Compile all the dependencies of this workspace + workspace.dependsOn.foreach(compileWorkspaceAndDependencies) + } s"""{ | "id" : "${workspace.name}", @@ -80,16 +84,21 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { /** Open the code in the given file and returns the file. * @param code code in file * @param fileName file path in the source directory + * @param openInIDE If true, send `textDocument/didOpen` to the server. * @return the file opened */ - def openCode(code: String, workspace: Workspace, fileName: String): TestFile = { + def openCode(code: String, workspace: Workspace, fileName: String, openInIDE: Boolean): TestFile = { val testFile = new TestFile(workspace.name + separator + fileName) - val dotdp = new DidOpenTextDocumentParams() val tdi = new TextDocumentItem() tdi.setUri(testFile.uri) tdi.setText(code) - dotdp.setTextDocument(tdi) - server.didOpen(dotdp) + + if (openInIDE) { + val dotdp = new DidOpenTextDocumentParams() + dotdp.setTextDocument(tdi) + server.didOpen(dotdp) + } + testFile } @@ -127,7 +136,7 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { private def compileWorkspace(workspace: Workspace): Unit = { val sourcesDir = sourceDirectory(workspace, wipe = true) val sources = workspace.sources.zipWithIndex.map { case (src, id) => - val path = sourcesDir.resolve(s"Source${id}.scala").toAbsolutePath + val path = sourcesDir.resolve(src.sourceName(id)).toAbsolutePath Files.write(path, src.text.getBytes("UTF-8")) path.toString } From 82d37e538caf3fad0a3ea0889b8cc9688ab59f32 Mon Sep 17 00:00:00 2001 From: Martin Duhem Date: Fri, 19 Oct 2018 09:15:21 +0200 Subject: [PATCH 7/7] Rename `Workspace` to `Project` --- .../tools/languageserver/DefinitionTest.scala | 10 +-- .../tools/languageserver/util/Code.scala | 46 +++++------ .../languageserver/util/CodeTester.scala | 24 +++--- .../util/server/TestServer.scala | 80 +++++++++---------- 4 files changed, 80 insertions(+), 80 deletions(-) diff --git a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala index d63c20831fea..d83ad9792ca5 100644 --- a/language-server/test/dotty/tools/languageserver/DefinitionTest.scala +++ b/language-server/test/dotty/tools/languageserver/DefinitionTest.scala @@ -37,20 +37,20 @@ class DefinitionTest { .definition(m13 to m14, List(m3 to m4)) } - @Test def classDefinitionDifferentWorkspace: Unit = { - val w0 = Workspace.withSources( + @Test def classDefinitionDifferentProject: Unit = { + val p0 = Project.withSources( code"""class ${m1}Foo${m2}""" ) - val w1 = Workspace.dependingOn(w0).withSources( + val p1 = Project.dependingOn(p0).withSources( code"""class Bar { new ${m3}Foo${m4} }""" ) - val w2 = Workspace.dependingOn(w1).withSources( + val p2 = Project.dependingOn(p1).withSources( code"""class Baz extends ${m5}Foo${m6}""" ) - withWorkspaces(w0, w1, w2) + withProjects(p0, p1, p2) .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/util/Code.scala b/language-server/test/dotty/tools/languageserver/util/Code.scala index 5594e9c914fb..81edf2c322d9 100644 --- a/language-server/test/dotty/tools/languageserver/util/Code.scala +++ b/language-server/test/dotty/tools/languageserver/util/Code.scala @@ -110,11 +110,11 @@ object Code { } } - /** A new `CodeTester` working with a single workspace containing `sources`. */ - def withSources(sources: SourceWithPositions*): CodeTester = withWorkspaces(Workspace(sources.toList)) + /** A new `CodeTester` working with a single project containing `sources`. */ + def withSources(sources: SourceWithPositions*): CodeTester = withProjects(Project(sources.toList)) - /** A new `CodeTester` working with `workspaces`. */ - def withWorkspaces(workspaces: Workspace*): CodeTester = new CodeTester(workspaces.toList) + /** A new `CodeTester` working with `projects`. */ + def withProjects(projects: Project*): CodeTester = new CodeTester(projects.toList) sealed trait SourceWithPositions { @@ -127,7 +127,7 @@ object Code { /** The positions of the markers that have been set. */ def positions: List[(CodeMarker, Int, Int)] - /** A new `CodeTester` with only this source in the workspace. */ + /** A new `CodeTester` with only this source in the project. */ def withSource: CodeTester = withSources(this) } @@ -165,40 +165,40 @@ object Code { /** * A group of sources belonging to the same project. * - * @param sources The sources that this workspace holds. - * @param name The name of this workspace - * @param dependsOn The other workspaces on which this workspace depend. + * @param sources The sources that this project holds. + * @param name The name of this project + * @param dependsOn The other projects on which this project depend. */ - case class Workspace(sources: List[SourceWithPositions], - name: String = Workspace.freshName, - dependsOn: List[Workspace] = Nil) { + case class Project(sources: List[SourceWithPositions], + name: String = Project.freshName, + dependsOn: List[Project] = Nil) { /** - * Add `sources` to the sources of this workspace. + * Add `sources` to the sources of this project. */ - def withSources(sources: SourceWithPositions*): Workspace = copy(sources = this.sources ::: sources.toList) + def withSources(sources: SourceWithPositions*): Project = copy(sources = this.sources ::: sources.toList) } - object Workspace { + object Project { private[this] val count = new java.util.concurrent.atomic.AtomicInteger() - private def freshName: String = s"workspace${count.incrementAndGet()}" + private def freshName: String = s"project${count.incrementAndGet()}" /** - * Creates a new workspace that depends on `workspaces`. + * Creates a new project that depends on `projects`. * - * @param workspaces The dependencies of the new workspace. - * @return An empty workspace with a dependency on the specified workspaces. + * @param projects The dependencies of the new project. + * @return An empty project with a dependency on the specified projects. */ - def dependingOn(workspaces: Workspace*) = new Workspace(Nil, dependsOn = workspaces.toList) + def dependingOn(projects: Project*) = new Project(Nil, dependsOn = projects.toList) /** - * Create a new workspace with the given sources. + * Create a new project with the given sources. * - * @param sources The sources to add to this workspace. - * @return a new workspace containing the specified sources. + * @param sources The sources to add to this project. + * @return a new project containing the specified sources. */ - def withSources(sources: SourceWithPositions*): Workspace = new Workspace(sources.toList) + def withSources(sources: SourceWithPositions*): Project = new Project(sources.toList) } } diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala index 7fef3161bad0..257208e98c9c 100644 --- a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -7,22 +7,22 @@ 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`. + * Simulates an LSP client for test in a project defined by `sources`. * - * @param sources The list of sources in the workspace + * @param sources The list of sources in the project */ -class CodeTester(workspaces: List[Workspace]) { +class CodeTester(projects: List[Project]) { - private val testServer = new TestServer(TestFile.testDir, workspaces) + private val testServer = new TestServer(TestFile.testDir, projects) - private val sources = for { workspace <- workspaces - source <- workspace.sources } yield (workspace, source) + private val sources = for { project <- projects + source <- project.sources } yield (project, source) private val files = - for { workspace <- workspaces - (source, id) <- workspace.sources.zipWithIndex } yield source match { - case src @ TastyWithPositions(text, _) => testServer.openCode(text, workspace, src.sourceName(id), openInIDE = false) - case other => testServer.openCode(other.text, workspace, other.sourceName(id), openInIDE = true) + for { project <- projects + (source, id) <- project.sources.zipWithIndex } yield source match { + case src @ TastyWithPositions(text, _) => testServer.openCode(text, project, src.sourceName(id), openInIDE = false) + case other => testServer.openCode(other.text, project, other.sourceName(id), openInIDE = true) } private val positions: PositionContext = getPositions(files) @@ -165,8 +165,8 @@ class CodeTester(workspaces: List[Workspace]) { case ex: AssertionError => val sourcesStr = sources.zip(files).map { - case ((workspace, source), file) => - s"""// ${file.file} in workspace ${workspace.name} + case ((project, source), file) => + s"""// ${file.file} in project ${project.name} |${source.text}""".stripMargin }.mkString(System.lineSeparator) diff --git a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala index 0996a92585bc..b8857a7c9241 100644 --- a/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala +++ b/language-server/test/dotty/tools/languageserver/util/server/TestServer.scala @@ -10,10 +10,10 @@ import dotty.tools.dotc.Main import dotty.tools.dotc.reporting.{Reporter, ThrowingReporter} import dotty.tools.io.Directory import dotty.tools.languageserver.DottyLanguageServer -import dotty.tools.languageserver.util.Code.{TastyWithPositions, Workspace} +import dotty.tools.languageserver.util.Code.{TastyWithPositions, Project} import org.eclipse.lsp4j.{ DidOpenTextDocumentParams, InitializeParams, InitializeResult, TextDocumentItem} -class TestServer(testFolder: Path, workspaces: List[Workspace]) { +class TestServer(testFolder: Path, projects: List[Project]) { val server = new DottyLanguageServer var client: TestClient = _ @@ -21,52 +21,52 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { init() private[this] def init(): InitializeResult = { - var compiledWorkspaces: Set[Workspace] = Set.empty - - /** Compile the dependencies of the given workspace, and then the workspace. */ - def compileWorkspaceAndDependencies(workspace: Workspace): Unit = - if (!compiledWorkspaces.contains(workspace)) { - workspace.dependsOn.foreach(compileWorkspaceAndDependencies) - compileWorkspace(workspace) - compiledWorkspaces += workspace + var compiledProjects: Set[Project] = Set.empty + + /** Compile the dependencies of the given project, and then the project. */ + def compileProjectAndDependencies(project: Project): Unit = + if (!compiledProjects.contains(project)) { + project.dependsOn.foreach(compileProjectAndDependencies) + compileProject(project) + compiledProjects += project } /** - * Set up given workspace, return JSON config. + * Set up given project, return JSON config. * - * If the workspace has dependencies, these dependencies are compiled. The classfiles of the - * dependent workspaces are put on the classpath of this workspace. + * If the project has dependencies, these dependencies are compiled. The classfiles of the + * dependent projects are put on the classpath of this project. * - * @param workspace The workspace to configure. - * @return A JSON object representing the configuration for this workspace. + * @param project The project to configure. + * @return A JSON object representing the configuration for this project. */ - def workspaceSetup(workspace: Workspace): String = { + def projectSetup(project: Project): String = { def showSeq[T](lst: Seq[T]): String = lst .map(elem => '"' + elem.toString.replace('\\', '/') + '"') .mkString("[ ", ", ", " ]") - if (workspace.sources.exists(_.isInstanceOf[TastyWithPositions])) { - compileWorkspaceAndDependencies(workspace) + if (project.sources.exists(_.isInstanceOf[TastyWithPositions])) { + compileProjectAndDependencies(project) } else { - // Compile all the dependencies of this workspace - workspace.dependsOn.foreach(compileWorkspaceAndDependencies) + // Compile all the dependencies of this project + project.dependsOn.foreach(compileProjectAndDependencies) } s"""{ - | "id" : "${workspace.name}", + | "id" : "${project.name}", | "compilerVersion" : "${BuildInfo.ideTestsCompilerVersion}", | "compilerArguments" : ${showSeq(BuildInfo.ideTestsCompilerArguments)}, - | "sourceDirectories" : ${showSeq(sourceDirectory(workspace, wipe = false) :: Nil)}, - | "dependencyClasspath" : ${showSeq(dependencyClasspath(workspace))}, - | "classDirectory" : "${classDirectory(workspace, wipe = false).toString.replace('\\','/')}" + | "sourceDirectories" : ${showSeq(sourceDirectory(project, wipe = false) :: Nil)}, + | "dependencyClasspath" : ${showSeq(dependencyClasspath(project))}, + | "classDirectory" : "${classDirectory(project, wipe = false).toString.replace('\\','/')}" |} |""".stripMargin } Files.createDirectories(testFolder) val configFile = testFolder.resolve(DottyLanguageServer.IDE_CONFIG_FILE) - val configuration = workspaces.map(workspaceSetup).mkString("[", ",", "]") + val configuration = projects.map(projectSetup).mkString("[", ",", "]") new PrintWriter(configFile.toString) { write(configuration) @@ -87,8 +87,8 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { * @param openInIDE If true, send `textDocument/didOpen` to the server. * @return the file opened */ - def openCode(code: String, workspace: Workspace, fileName: String, openInIDE: Boolean): TestFile = { - val testFile = new TestFile(workspace.name + separator + fileName) + def openCode(code: String, project: Project, fileName: String, openInIDE: Boolean): TestFile = { + val testFile = new TestFile(project.name + separator + fileName) val tdi = new TextDocumentItem() tdi.setUri(testFile.uri) tdi.setText(code) @@ -102,8 +102,8 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { testFile } - private def classDirectory(workspace: Workspace, wipe: Boolean): Path = { - val path = testFolder.resolve(workspace.name).resolve("out") + private def classDirectory(project: Project, wipe: Boolean): Path = { + val path = testFolder.resolve(project.name).resolve("out") if (wipe) { Directory(path).deleteRecursively() Files.createDirectories(path) @@ -111,15 +111,15 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { path.toAbsolutePath } - private def dependencyClasspath(workspace: Workspace): Seq[String] = { + private def dependencyClasspath(project: Project): Seq[String] = { BuildInfo.ideTestsDependencyClasspath.map(_.getAbsolutePath) ++ - workspace.dependsOn.flatMap { dep => + project.dependsOn.flatMap { dep => classDirectory(dep, wipe = false).toString +: dependencyClasspath(dep) } }.distinct - private def sourceDirectory(workspace: Workspace, wipe: Boolean): Path = { - val path = TestFile.sourceDir.resolve(workspace.name).toAbsolutePath + private def sourceDirectory(project: Project, wipe: Boolean): Path = { + val path = TestFile.sourceDir.resolve(project.name).toAbsolutePath if (wipe) { Directory(path).deleteRecursively() Files.createDirectories(path) @@ -128,14 +128,14 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { } /** - * Sets up the sources of the given workspace, creates the necessary directories + * Sets up the sources of the given project, creates the necessary directories * and compile the sources. * - * @param workspace The workspace to set up. + * @param project The project to set up. */ - private def compileWorkspace(workspace: Workspace): Unit = { - val sourcesDir = sourceDirectory(workspace, wipe = true) - val sources = workspace.sources.zipWithIndex.map { case (src, id) => + private def compileProject(project: Project): Unit = { + val sourcesDir = sourceDirectory(project, wipe = true) + val sources = project.sources.zipWithIndex.map { case (src, id) => val path = sourcesDir.resolve(src.sourceName(id)).toAbsolutePath Files.write(path, src.text.getBytes("UTF-8")) path.toString @@ -144,8 +144,8 @@ class TestServer(testFolder: Path, workspaces: List[Workspace]) { val compileOptions = sources.toArray ++ Array( - "-classpath", dependencyClasspath(workspace).mkString(pathSeparator), - "-d", classDirectory(workspace, wipe = true).toString + "-classpath", dependencyClasspath(project).mkString(pathSeparator), + "-d", classDirectory(project, wipe = true).toString ) val reporter = new ThrowingReporter(Reporter.NoReporter) Main.process(compileOptions, reporter)