diff --git a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala index e30a0b587e37..8c0e69afc5a6 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala @@ -519,4 +519,26 @@ object Interactive { } } + /** + * Return a predicate function that determines whether a given `NameTree` is an implementation of + * `sym`. + * + * @param sym The symbol whose implementations to find. + * @return A function that determines whether a `NameTree` is an implementation of `sym`. + */ + def implementationFilter(sym: Symbol)(implicit ctx: Context): NameTree => Boolean = { + if (sym.isClass) { + case td: TypeDef => + val treeSym = td.symbol + (treeSym != sym || !treeSym.is(AbstractOrTrait)) && treeSym.derivesFrom(sym) + case _ => + false + } else { + case md: MemberDef => + matchSymbol(md, sym, Include.overriding) && !md.symbol.is(Deferred) + case _ => + false + } + } + } diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index 736d1f335d77..93b256f1a797 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -194,6 +194,7 @@ class DottyLanguageServer extends LanguageServer c.setHoverProvider(true) c.setWorkspaceSymbolProvider(true) c.setReferencesProvider(true) + c.setImplementationProvider(true) c.setCompletionProvider(new CompletionOptions( /* resolveProvider = */ false, /* triggerCharacters = */ List(".").asJava)) @@ -313,39 +314,20 @@ class DottyLanguageServer extends LanguageServer val pos = sourcePosition(driver, uri, params.getPosition) - val (definitions, projectsToInspect, originalSymbol, originalSymbolName) = { + val (definitions, originalSymbol, originalSymbolName) = { implicit val ctx: Context = driver.currentCtx val path = Interactive.pathTo(driver.openedTrees(uri), pos) val originalSymbol = Interactive.enclosingSourceSymbol(path) val originalSymbolName = originalSymbol.name.sourceModuleName.toString - - // Find definitions of the symbol under the cursor, so that we can determine - // what projects are worth exploring val definitions = Interactive.findDefinitions(path, driver) - val projectsToInspect = - if (definitions.isEmpty) { - drivers.keySet - } else { - for { - definition <- definitions - uri <- toUriOption(definition.pos.source).toSet - config = configFor(uri) - project <- dependentProjects(config) + config - } yield project - } - (definitions, projectsToInspect, originalSymbol, originalSymbolName) + (definitions, originalSymbol, originalSymbolName) } val references = { // Collect the information necessary to look into each project separately: representation of // `originalSymbol` in this project, the context and correct Driver. - val perProjectInfo = projectsToInspect.toList.map { config => - val remoteDriver = drivers(config) - val ctx = remoteDriver.currentCtx - val definition = Interactive.localize(originalSymbol, driver, remoteDriver) - (remoteDriver, ctx, definition) - } + val perProjectInfo = inProjectsSeeing(driver, definitions, originalSymbol) perProjectInfo.flatMap { (remoteDriver, ctx, definition) => val trees = remoteDriver.sourceTreesContaining(originalSymbolName)(ctx) @@ -447,6 +429,34 @@ class DottyLanguageServer extends LanguageServer }.asJava } + override def implementation(params: TextDocumentPositionParams) = computeAsync { cancelToken => + val uri = new URI(params.getTextDocument.getUri) + val driver = driverFor(uri) + + val pos = sourcePosition(driver, uri, params.getPosition) + + val (definitions, originalSymbol) = { + implicit val ctx: Context = driver.currentCtx + val path = Interactive.pathTo(driver.openedTrees(uri), pos) + val originalSymbol = Interactive.enclosingSourceSymbol(path) + val definitions = Interactive.findDefinitions(path, driver) + (definitions, originalSymbol) + } + + val implementations = { + val perProjectInfo = inProjectsSeeing(driver, definitions, originalSymbol) + + perProjectInfo.flatMap { (remoteDriver, ctx, definition) => + val trees = remoteDriver.sourceTrees(ctx) + val predicate = Interactive.implementationFilter(definition)(ctx) + val matches = Interactive.namedTrees(trees, includeReferences = false, predicate)(ctx) + matches.map(tree => location(tree.namePos(ctx), positionMapperFor(tree.source))) + } + }.toList + + implementations.flatten.asJava + } + override def getTextDocumentService: TextDocumentService = this override def getWorkspaceService: WorkspaceService = this @@ -460,6 +470,48 @@ class DottyLanguageServer extends LanguageServer override def resolveCodeLens(params: CodeLens) = null override def resolveCompletionItem(params: CompletionItem) = null override def signatureHelp(params: TextDocumentPositionParams) = null + + /** + * Find the set of projects that have any of `definitions` on their classpath. + * + * @param definitions The definitions to consider when looking for projects. + * @return The set of projects that have any of `definitions` on their classpath. + */ + private def projectsSeeing(definitions: List[SourceTree])(implicit ctx: Context): Set[ProjectConfig] = { + if (definitions.isEmpty) { + drivers.keySet + } else { + for { + definition <- definitions.toSet + uri <- toUriOption(definition.pos.source).toSet + config = configFor(uri) + project <- dependentProjects(config) + config + } yield project + } + } + + /** + * Finds projects that can see any of `definitions`, translate `symbol` in their universe. + * + * @param baseDriver The driver responsible for the trees in `definitions` and `symbol`. + * @param definitions The definitions to consider when looking for projects. + * @param symbol A symbol to translate in the universes of the remote projects. + * @return A list consisting of the remote drivers, their context, and the translation of `symbol` + * into their universe. + */ + private def inProjectsSeeing(baseDriver: InteractiveDriver, + definitions: List[SourceTree], + symbol: Symbol): List[(InteractiveDriver, Context, Symbol)] = { + val projects = projectsSeeing(definitions)(baseDriver.currentCtx) + projects.toList.map { config => + val remoteDriver = drivers(config) + val ctx = remoteDriver.currentCtx + val definition = Interactive.localize(symbol, baseDriver, remoteDriver) + (remoteDriver, ctx, definition) + } + } + + } object DottyLanguageServer { diff --git a/language-server/test/dotty/tools/languageserver/ImplementationTest.scala b/language-server/test/dotty/tools/languageserver/ImplementationTest.scala new file mode 100644 index 000000000000..cb5a8faaf18c --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/ImplementationTest.scala @@ -0,0 +1,76 @@ +package dotty.tools.languageserver + +import dotty.tools.languageserver.util.Code._ + +import org.junit.Test + +class ImplementationTest { + + @Test def implMethodFromTrait: Unit = { + code"""trait A { + def ${m1}foo${m2}(x: Int): String + } + class B extends A { + override def ${m3}foo${m4}(x: Int): String = "" + }""".withSource + .implementation(m1 to m2, List(m3 to m4)) + .implementation(m3 to m4, List(m3 to m4)) + } + + @Test def implMethodFromTrait0: Unit = { + code"""trait A { + def ${m1}foo${m2}(x: Int): String + } + class B extends A { + override def ${m3}foo${m4}(x: Int): String = "" + } + class C extends B { + override def ${m5}foo${m6}(x: Int): String = "" + }""".withSource + .implementation(m1 to m2, List(m3 to m4, m5 to m6)) + .implementation(m3 to m4, List(m3 to m4, m5 to m6)) + .implementation(m5 to m6, List(m5 to m6)) + } + + @Test def extendsTrait: Unit = { + code"""trait ${m1}A${m2} + class ${m3}B${m4} extends ${m5}A${m6}""".withSource + .implementation(m1 to m2, List(m3 to m4)) + .implementation(m3 to m4, List(m3 to m4)) + .implementation(m5 to m6, List(m3 to m4)) + } + + @Test def extendsClass: Unit = { + code"""class ${m1}A${m2} + class ${m3}B${m4} extends ${m5}A${m6}""".withSource + .implementation(m1 to m2, List(m1 to m2, m3 to m4)) + .implementation(m3 to m4, List(m3 to m4)) + .implementation(m5 to m6, List(m1 to m2, m3 to m4)) + } + + @Test def objExtendsTrait: Unit = { + code"""trait ${m1}A${m2} + object ${m3}B${m4} extends ${m5}A${m6}""".withSource + .implementation(m1 to m2, List(m3 to m4)) + .implementation(m3 to m4, List(m3 to m4)) + .implementation(m5 to m6, List(m3 to m4)) + } + + @Test def defineAbstractType: Unit = { + code"""trait A { type ${m1}T${m2} } + trait B extends A { type ${m3}T${m4} = Int }""".withSource + .implementation(m1 to m2, List(m3 to m4)) + .implementation(m3 to m4, List(m3 to m4)) + } + + @Test def innerClass: Unit = { + code"""trait A { trait ${m1}AA${m2} } + class B extends A { + class ${m3}AB${m4} extends ${m5}AA${m6} + }""".withSource + .implementation(m1 to m2, List(m3 to m4)) + .implementation(m3 to m4, List(m3 to m4)) + .implementation(m5 to m6, List(m3 to m4)) + } + +} diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala index 257208e98c9c..6697c5c28a10 100644 --- a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -158,6 +158,15 @@ class CodeTester(projects: List[Project]) { def cancelRun(marker: CodeMarker, afterMs: Long): this.type = doAction(new WorksheetCancel(marker, afterMs)) + /** + * Find implementations of the symbol in `range`, compares that the results match `expected. + * + * @param range The range of position over which to run `textDocument/implementation`. + * @param expected The expected result. + */ + def implementation(range: CodeRange, expected: List[CodeRange]): this.type = + doAction(new Implementation(range, expected)) + private def doAction(action: Action): this.type = { try { action.execute()(testServer, testServer.client, positions) diff --git a/language-server/test/dotty/tools/languageserver/util/actions/Implementation.scala b/language-server/test/dotty/tools/languageserver/util/actions/Implementation.scala new file mode 100644 index 000000000000..5ab220e687ec --- /dev/null +++ b/language-server/test/dotty/tools/languageserver/util/actions/Implementation.scala @@ -0,0 +1,36 @@ +package dotty.tools.languageserver.util.actions + +import dotty.tools.languageserver.util.embedded.CodeMarker +import dotty.tools.languageserver.util.{CodeRange, PositionContext} + +import org.eclipse.lsp4j.Location + +import org.junit.Assert.assertEquals + +import scala.collection.JavaConverters._ + +/** + * An action requesting the implementations of the symbol inside `range`. + * This action corresponds to the `textDocument/implementation` method of the Language Server + * Protocol. + * + * @param range The range of position for which to request implementations. + * @param expected The expected results. + */ +class Implementation(override val range: CodeRange, expected: List[CodeRange]) extends ActionOnRange { + + private implicit val LocationOrdering: Ordering[Location] = Ordering.by(_.toString) + + override def onMarker(marker: CodeMarker): Exec[Unit] = { + val expectedLocations = expected.map(_.toLocation) + val results: Seq[org.eclipse.lsp4j.Location] = server.implementation(marker.toTextDocumentPositionParams).get().asScala + + assertEquals(expectedLocations.length, results.length) + expectedLocations.sorted.zip(results.sorted).foreach { + assertEquals(_, _) + } + } + + override def show: PositionContext.PosCtx[String] = + s"Implementation(${range.show}, $expected)" +}