Skip to content

IDE: Support textDocument/implementation #5401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Nov 13, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions compiler/src/dotty/tools/dotc/interactive/Interactive.scala
Original file line number Diff line number Diff line change
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
}
}

}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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))
}

}
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)"
}