Skip to content

Commit 8477cd4

Browse files
committed
IDE: Support textDocument/implementation
This is used to find implementations of the symbol under the cursor.
1 parent ed6743c commit 8477cd4

File tree

5 files changed

+189
-9
lines changed

5 files changed

+189
-9
lines changed

compiler/src/dotty/tools/dotc/interactive/Interactive.scala

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -518,4 +518,26 @@ object Interactive {
518518
}
519519
}
520520

521+
/**
522+
* Return a predicate function that determines whether a given `NameTree` is an implementation of
523+
* `sym`.
524+
*
525+
* @param sym The symbol whose implementations to find.
526+
* @return A function that determines whether a `NameTree` is an implementation of `sym`.
527+
*/
528+
def isImplementation(sym: Symbol)(implicit ctx: Context): NameTree => Boolean = {
529+
if (sym.isClass) {
530+
case td: TypeDef =>
531+
val treeSym = td.symbol
532+
(treeSym != sym || !treeSym.is(AbstractOrTrait)) && treeSym.derivesFrom(sym)
533+
case _ =>
534+
false
535+
} else {
536+
case md: MemberDef =>
537+
matchSymbol(md, sym, Include.overriding) && !md.symbol.is(Deferred)
538+
case _ =>
539+
false
540+
}
541+
}
542+
521543
}

language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 46 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ class DottyLanguageServer extends LanguageServer
194194
c.setHoverProvider(true)
195195
c.setWorkspaceSymbolProvider(true)
196196
c.setReferencesProvider(true)
197+
c.setImplementationProvider(true)
197198
c.setCompletionProvider(new CompletionOptions(
198199
/* resolveProvider = */ false,
199200
/* triggerCharacters = */ List(".").asJava))
@@ -318,15 +319,7 @@ class DottyLanguageServer extends LanguageServer
318319
// Find definitions of the symbol under the cursor, so that we can determine
319320
// what projects are worth exploring
320321
val definitions = Interactive.findDefinitions(path, driver)
321-
val projectsToInspect =
322-
if (definitions.isEmpty) {
323-
drivers.keySet
324-
} else {
325-
definitions.flatMap { definition =>
326-
val config = configFor(toUri(definition.pos.source))
327-
dependentProjects(config) + config
328-
}
329-
}
322+
val projectsToInspect = projectsSeeing(definitions)
330323

331324
val originalSymbol = Interactive.enclosingSourceSymbol(path)
332325
val symbolName = originalSymbol.name.sourceModuleName.toString
@@ -438,6 +431,38 @@ class DottyLanguageServer extends LanguageServer
438431
}.asJava
439432
}
440433

434+
override def implementation(params: TextDocumentPositionParams) = computeAsync { cancelToken =>
435+
val uri = new URI(params.getTextDocument.getUri)
436+
val driver = driverFor(uri)
437+
implicit val ctx = driver.currentCtx
438+
439+
val pos = sourcePosition(driver, uri, params.getPosition)
440+
val uriTrees = driver.openedTrees(uri)
441+
val path = Interactive.pathTo(uriTrees, pos)
442+
443+
val definitions = Interactive.findDefinitions(path, driver)
444+
val projectsToInspect = projectsSeeing(definitions)
445+
446+
val originalSymbol = Interactive.enclosingSourceSymbol(path)
447+
val implementations = {
448+
val perProjectInfo = projectsToInspect.toList.map { config =>
449+
val remoteDriver = drivers(config)
450+
val ctx = remoteDriver.currentCtx
451+
val definition = Interactive.localize(originalSymbol, driver, remoteDriver)
452+
(remoteDriver, ctx, definition)
453+
}
454+
455+
perProjectInfo.par.flatMap { (remoteDriver, ctx, definition) =>
456+
val trees = remoteDriver.sourceTrees(ctx)
457+
Interactive.namedTrees(trees, includeReferences = false, Interactive.isImplementation(definition)(ctx))(ctx)
458+
}
459+
}.toList
460+
461+
implementations.flatMap { impl =>
462+
location(impl.namePos, positionMapperFor(impl.source))
463+
}.asJava
464+
}
465+
441466
override def getTextDocumentService: TextDocumentService = this
442467
override def getWorkspaceService: WorkspaceService = this
443468

@@ -451,6 +476,18 @@ class DottyLanguageServer extends LanguageServer
451476
override def resolveCodeLens(params: CodeLens) = null
452477
override def resolveCompletionItem(params: CompletionItem) = null
453478
override def signatureHelp(params: TextDocumentPositionParams) = null
479+
480+
private def projectsSeeing(definitions: List[SourceTree])(implicit ctx: Context): Set[ProjectConfig] = {
481+
if (definitions.isEmpty) {
482+
drivers.keySet
483+
} else {
484+
definitions.toSet.flatMap { definition =>
485+
val config = configFor(toUri(definition.pos.source))
486+
dependentProjects(config) + config
487+
}
488+
}
489+
}
490+
454491
}
455492

456493
object DottyLanguageServer {
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package dotty.tools.languageserver
2+
3+
import dotty.tools.languageserver.util.Code._
4+
5+
import org.junit.Test
6+
7+
class ImplementationTest {
8+
9+
@Test def implMethodFromTrait: Unit = {
10+
code"""trait A {
11+
def ${m1}foo${m2}(x: Int): String
12+
}
13+
class B extends A {
14+
override def ${m3}foo${m4}(x: Int): String = ""
15+
}""".withSource
16+
.implementation(m1 to m2, List(m3 to m4))
17+
.implementation(m3 to m4, List(m3 to m4))
18+
}
19+
20+
@Test def implMethodFromTrait0: Unit = {
21+
code"""trait A {
22+
def ${m1}foo${m2}(x: Int): String
23+
}
24+
class B extends A {
25+
override def ${m3}foo${m4}(x: Int): String = ""
26+
}
27+
class C extends B {
28+
override def ${m5}foo${m6}(x: Int): String = ""
29+
}""".withSource
30+
.implementation(m1 to m2, List(m3 to m4, m5 to m6))
31+
.implementation(m3 to m4, List(m3 to m4, m5 to m6))
32+
.implementation(m5 to m6, List(m5 to m6))
33+
}
34+
35+
@Test def extendsTrait: Unit = {
36+
code"""trait ${m1}A${m2}
37+
class ${m3}B${m4} extends ${m5}A${m6}""".withSource
38+
.implementation(m1 to m2, List(m3 to m4))
39+
.implementation(m3 to m4, List(m3 to m4))
40+
.implementation(m5 to m6, List(m3 to m4))
41+
}
42+
43+
@Test def extendsClass: Unit = {
44+
code"""class ${m1}A${m2}
45+
class ${m3}B${m4} extends ${m5}A${m6}""".withSource
46+
.implementation(m1 to m2, List(m1 to m2, m3 to m4))
47+
.implementation(m3 to m4, List(m3 to m4))
48+
.implementation(m5 to m6, List(m1 to m2, m3 to m4))
49+
}
50+
51+
@Test def objExtendsTrait: Unit = {
52+
code"""trait ${m1}A${m2}
53+
object ${m3}B${m4} extends ${m5}A${m6}""".withSource
54+
.implementation(m1 to m2, List(m3 to m4))
55+
.implementation(m3 to m4, List(m3 to m4))
56+
.implementation(m5 to m6, List(m3 to m4))
57+
}
58+
59+
@Test def defineAbstractType: Unit = {
60+
code"""trait A { type ${m1}T${m2} }
61+
trait B extends A { type ${m3}T${m4} = Int }""".withSource
62+
.implementation(m1 to m2, List(m3 to m4))
63+
.implementation(m3 to m4, List(m3 to m4))
64+
}
65+
66+
@Test def innerClass: Unit = {
67+
code"""trait A { trait ${m1}AA${m2} }
68+
class B extends A {
69+
class ${m3}AB${m4} extends ${m5}AA${m6}
70+
}""".withSource
71+
.implementation(m1 to m2, List(m3 to m4))
72+
.implementation(m3 to m4, List(m3 to m4))
73+
.implementation(m5 to m6, List(m3 to m4))
74+
}
75+
76+
}

language-server/test/dotty/tools/languageserver/util/CodeTester.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,15 @@ class CodeTester(projects: List[Project]) {
158158
def cancelRun(marker: CodeMarker, afterMs: Long): this.type =
159159
doAction(new WorksheetCancel(marker, afterMs))
160160

161+
/**
162+
* Find implementations of the symbol in `range`, compares that the results match `expected.
163+
*
164+
* @param range The range of position over which to run `textDocument/implementation`.
165+
* @param expected The expected result.
166+
*/
167+
def implementation(range: CodeRange, expected: List[CodeRange]): this.type =
168+
doAction(new Implementation(range, expected))
169+
161170
private def doAction(action: Action): this.type = {
162171
try {
163172
action.execute()(testServer, testServer.client, positions)
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package dotty.tools.languageserver.util.actions
2+
3+
import dotty.tools.languageserver.util.embedded.CodeMarker
4+
import dotty.tools.languageserver.util.{CodeRange, PositionContext}
5+
6+
import org.eclipse.lsp4j.Location
7+
8+
import org.junit.Assert.assertEquals
9+
10+
import scala.collection.JavaConverters._
11+
12+
/**
13+
* An action requesting the implementations of the symbol inside `range`.
14+
* This action corresponds to the `textDocument/implementation` method of the Language Server
15+
* Protocol.
16+
*
17+
* @param range The range of position for which to request implementations.
18+
* @param expected The expected results.
19+
*/
20+
class Implementation(override val range: CodeRange, expected: List[CodeRange]) extends ActionOnRange {
21+
22+
private implicit val LocationOrdering: Ordering[Location] = Ordering.by(_.toString)
23+
24+
override def onMarker(marker: CodeMarker): Exec[Unit] = {
25+
val expectedLocations = expected.map(_.toLocation)
26+
val results: Seq[org.eclipse.lsp4j.Location] = server.implementation(marker.toTextDocumentPositionParams).get().asScala
27+
28+
assertEquals(expectedLocations.length, results.length)
29+
expectedLocations.sorted.zip(results.sorted).foreach {
30+
assertEquals(_, _)
31+
}
32+
}
33+
34+
override def show: PositionContext.PosCtx[String] =
35+
s"Implementation(${range.show}, $expected)"
36+
}

0 commit comments

Comments
 (0)