Skip to content

Commit bdd932f

Browse files
authored
Merge pull request #5401 from dotty-staging/topic/ide-implementation
IDE: Support `textDocument/implementation`
2 parents e27b6fa + 858731b commit bdd932f

File tree

5 files changed

+217
-22
lines changed

5 files changed

+217
-22
lines changed

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

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

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

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

Lines changed: 74 additions & 22 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))
@@ -313,39 +314,20 @@ class DottyLanguageServer extends LanguageServer
313314

314315
val pos = sourcePosition(driver, uri, params.getPosition)
315316

316-
val (definitions, projectsToInspect, originalSymbol, originalSymbolName) = {
317+
val (definitions, originalSymbol, originalSymbolName) = {
317318
implicit val ctx: Context = driver.currentCtx
318319
val path = Interactive.pathTo(driver.openedTrees(uri), pos)
319320
val originalSymbol = Interactive.enclosingSourceSymbol(path)
320321
val originalSymbolName = originalSymbol.name.sourceModuleName.toString
321-
322-
// Find definitions of the symbol under the cursor, so that we can determine
323-
// what projects are worth exploring
324322
val definitions = Interactive.findDefinitions(path, driver)
325-
val projectsToInspect =
326-
if (definitions.isEmpty) {
327-
drivers.keySet
328-
} else {
329-
for {
330-
definition <- definitions
331-
uri <- toUriOption(definition.pos.source).toSet
332-
config = configFor(uri)
333-
project <- dependentProjects(config) + config
334-
} yield project
335-
}
336323

337-
(definitions, projectsToInspect, originalSymbol, originalSymbolName)
324+
(definitions, originalSymbol, originalSymbolName)
338325
}
339326

340327
val references = {
341328
// Collect the information necessary to look into each project separately: representation of
342329
// `originalSymbol` in this project, the context and correct Driver.
343-
val perProjectInfo = projectsToInspect.toList.map { config =>
344-
val remoteDriver = drivers(config)
345-
val ctx = remoteDriver.currentCtx
346-
val definition = Interactive.localize(originalSymbol, driver, remoteDriver)
347-
(remoteDriver, ctx, definition)
348-
}
330+
val perProjectInfo = inProjectsSeeing(driver, definitions, originalSymbol)
349331

350332
perProjectInfo.flatMap { (remoteDriver, ctx, definition) =>
351333
val trees = remoteDriver.sourceTreesContaining(originalSymbolName)(ctx)
@@ -447,6 +429,34 @@ class DottyLanguageServer extends LanguageServer
447429
}.asJava
448430
}
449431

432+
override def implementation(params: TextDocumentPositionParams) = computeAsync { cancelToken =>
433+
val uri = new URI(params.getTextDocument.getUri)
434+
val driver = driverFor(uri)
435+
436+
val pos = sourcePosition(driver, uri, params.getPosition)
437+
438+
val (definitions, originalSymbol) = {
439+
implicit val ctx: Context = driver.currentCtx
440+
val path = Interactive.pathTo(driver.openedTrees(uri), pos)
441+
val originalSymbol = Interactive.enclosingSourceSymbol(path)
442+
val definitions = Interactive.findDefinitions(path, driver)
443+
(definitions, originalSymbol)
444+
}
445+
446+
val implementations = {
447+
val perProjectInfo = inProjectsSeeing(driver, definitions, originalSymbol)
448+
449+
perProjectInfo.flatMap { (remoteDriver, ctx, definition) =>
450+
val trees = remoteDriver.sourceTrees(ctx)
451+
val predicate = Interactive.implementationFilter(definition)(ctx)
452+
val matches = Interactive.namedTrees(trees, includeReferences = false, predicate)(ctx)
453+
matches.map(tree => location(tree.namePos(ctx), positionMapperFor(tree.source)))
454+
}
455+
}.toList
456+
457+
implementations.flatten.asJava
458+
}
459+
450460
override def getTextDocumentService: TextDocumentService = this
451461
override def getWorkspaceService: WorkspaceService = this
452462

@@ -460,6 +470,48 @@ class DottyLanguageServer extends LanguageServer
460470
override def resolveCodeLens(params: CodeLens) = null
461471
override def resolveCompletionItem(params: CompletionItem) = null
462472
override def signatureHelp(params: TextDocumentPositionParams) = null
473+
474+
/**
475+
* Find the set of projects that have any of `definitions` on their classpath.
476+
*
477+
* @param definitions The definitions to consider when looking for projects.
478+
* @return The set of projects that have any of `definitions` on their classpath.
479+
*/
480+
private def projectsSeeing(definitions: List[SourceTree])(implicit ctx: Context): Set[ProjectConfig] = {
481+
if (definitions.isEmpty) {
482+
drivers.keySet
483+
} else {
484+
for {
485+
definition <- definitions.toSet
486+
uri <- toUriOption(definition.pos.source).toSet
487+
config = configFor(uri)
488+
project <- dependentProjects(config) + config
489+
} yield project
490+
}
491+
}
492+
493+
/**
494+
* Finds projects that can see any of `definitions`, translate `symbol` in their universe.
495+
*
496+
* @param baseDriver The driver responsible for the trees in `definitions` and `symbol`.
497+
* @param definitions The definitions to consider when looking for projects.
498+
* @param symbol A symbol to translate in the universes of the remote projects.
499+
* @return A list consisting of the remote drivers, their context, and the translation of `symbol`
500+
* into their universe.
501+
*/
502+
private def inProjectsSeeing(baseDriver: InteractiveDriver,
503+
definitions: List[SourceTree],
504+
symbol: Symbol): List[(InteractiveDriver, Context, Symbol)] = {
505+
val projects = projectsSeeing(definitions)(baseDriver.currentCtx)
506+
projects.toList.map { config =>
507+
val remoteDriver = drivers(config)
508+
val ctx = remoteDriver.currentCtx
509+
val definition = Interactive.localize(symbol, baseDriver, remoteDriver)
510+
(remoteDriver, ctx, definition)
511+
}
512+
}
513+
514+
463515
}
464516

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