Skip to content

Commit 4b6c383

Browse files
committed
IDE: Support textDocument/implementation
This is used to find implementations of the symbol under the cursor.
1 parent 6a315ff commit 4b6c383

File tree

5 files changed

+197
-11
lines changed

5 files changed

+197
-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
@@ -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: 54 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))
@@ -322,17 +323,7 @@ class DottyLanguageServer extends LanguageServer
322323
// Find definitions of the symbol under the cursor, so that we can determine
323324
// what projects are worth exploring
324325
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-
}
326+
val projectsToInspect = projectsSeeing(definitions)
336327

337328
(definitions, projectsToInspect, originalSymbol, originalSymbolName)
338329
}
@@ -447,6 +438,44 @@ class DottyLanguageServer extends LanguageServer
447438
}.asJava
448439
}
449440

441+
override def implementation(params: TextDocumentPositionParams) = computeAsync { cancelToken =>
442+
val uri = new URI(params.getTextDocument.getUri)
443+
val driver = driverFor(uri)
444+
445+
val pos = sourcePosition(driver, uri, params.getPosition)
446+
447+
val (definitions, projectsToInspect, originalSymbol) = {
448+
implicit val ctx: Context = driver.currentCtx
449+
val path = Interactive.pathTo(driver.openedTrees(uri), pos)
450+
val originalSymbol = Interactive.enclosingSourceSymbol(path)
451+
452+
// Find definitions of the symbol under the cursor, so that we can determine what projects are
453+
// worth exploring
454+
val definitions = Interactive.findDefinitions(path, driver)
455+
val projectsToInspect = projectsSeeing(definitions)
456+
457+
(definitions, projectsToInspect, originalSymbol)
458+
}
459+
460+
val implementations = {
461+
val perProjectInfo = projectsToInspect.toList.map { config =>
462+
val remoteDriver = drivers(config)
463+
val ctx = remoteDriver.currentCtx
464+
val definition = Interactive.localize(originalSymbol, driver, remoteDriver)
465+
(remoteDriver, ctx, definition)
466+
}
467+
468+
perProjectInfo.flatMap { (remoteDriver, ctx, definition) =>
469+
val trees = remoteDriver.sourceTrees(ctx)
470+
val predicate = Interactive.implementationFilter(definition)(ctx)
471+
val matches = Interactive.namedTrees(trees, includeReferences = false, predicate)(ctx)
472+
matches.map(tree => location(tree.namePos(ctx), positionMapperFor(tree.source)))
473+
}
474+
}.toList
475+
476+
implementations.flatten.asJava
477+
}
478+
450479
override def getTextDocumentService: TextDocumentService = this
451480
override def getWorkspaceService: WorkspaceService = this
452481

@@ -460,6 +489,20 @@ class DottyLanguageServer extends LanguageServer
460489
override def resolveCodeLens(params: CodeLens) = null
461490
override def resolveCompletionItem(params: CompletionItem) = null
462491
override def signatureHelp(params: TextDocumentPositionParams) = null
492+
493+
private def projectsSeeing(definitions: List[SourceTree])(implicit ctx: Context): Set[ProjectConfig] = {
494+
if (definitions.isEmpty) {
495+
drivers.keySet
496+
} else {
497+
for {
498+
definition <- definitions.toSet
499+
uri <- toUriOption(definition.pos.source).toSet
500+
config = configFor(uri)
501+
project <- dependentProjects(config) + config
502+
} yield project
503+
}
504+
}
505+
463506
}
464507

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