Skip to content

Commit

Permalink
Merge pull request #5401 from dotty-staging/topic/ide-implementation
Browse files Browse the repository at this point in the history
 IDE: Support `textDocument/implementation`
  • Loading branch information
Duhemm committed Nov 13, 2018
2 parents e27b6fa + 858731b commit bdd932f
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 22 deletions.
22 changes: 22 additions & 0 deletions compiler/src/dotty/tools/dotc/interactive/Interactive.scala
Expand Up @@ -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
}
}

}
Expand Up @@ -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))
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -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 {
Expand Down
@@ -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))
}

}
Expand Up @@ -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)
Expand Down
@@ -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)"
}

0 comments on commit bdd932f

Please sign in to comment.