Skip to content

Commit 5844ae0

Browse files
committed
IDE: Support textDocument/implementation
This is used to find implementations of the symbol under the cursor.
1 parent 0f8853a commit 5844ae0

File tree

5 files changed

+191
-11
lines changed

5 files changed

+191
-11
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: 48 additions & 11 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,17 +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-
for {
326-
definition <- definitions
327-
uri <- toUriOption(definition.pos.source).toSet
328-
config = configFor(uri)
329-
project <- dependentProjects(config) + config
330-
} yield project
331-
}
322+
val projectsToInspect = projectsSeeing(definitions)
332323

333324
val originalSymbol = Interactive.enclosingSourceSymbol(path)
334325
val symbolName = originalSymbol.name.sourceModuleName.toString
@@ -441,6 +432,38 @@ class DottyLanguageServer extends LanguageServer
441432
}.asJava
442433
}
443434

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

@@ -454,6 +477,20 @@ class DottyLanguageServer extends LanguageServer
454477
override def resolveCodeLens(params: CodeLens) = null
455478
override def resolveCompletionItem(params: CompletionItem) = null
456479
override def signatureHelp(params: TextDocumentPositionParams) = null
480+
481+
private def projectsSeeing(definitions: List[SourceTree])(implicit ctx: Context): Set[ProjectConfig] = {
482+
if (definitions.isEmpty) {
483+
drivers.keySet
484+
} else {
485+
for {
486+
definition <- definitions.toSet
487+
uri <- toUriOption(definition.pos.source).toSet
488+
config = configFor(uri)
489+
project <- dependentProjects(config) + config
490+
} yield project
491+
}
492+
}
493+
457494
}
458495

459496
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)