Skip to content

Commit ea8afdf

Browse files
committed
Fix #3979: Completion for renamed imports
We introduce a new `RenameAwareScope` that tracks the name associated with a symbol in the scope. Calling `toListWithNames` returns the list of symbols in this scope along with the name that should be used to refer to the symbol in this scope. Fixes #3979
1 parent 10e3380 commit ea8afdf

File tree

3 files changed

+74
-24
lines changed

3 files changed

+74
-24
lines changed

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

+49-24
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ object Completion {
137137

138138
private class CompletionBuffer(val mode: Mode, val prefix: String, pos: SourcePosition) {
139139

140-
private[this] val completions = Scopes.newScope.openForMutations
140+
private[this] val completions = new RenameAwareScope
141141

142142
/**
143143
* Return the list of symbols that shoudl be included in completion results.
@@ -146,7 +146,11 @@ object Completion {
146146
* the same `Completion`.
147147
*/
148148
def getCompletions(implicit ctx: Context): List[Completion] = {
149-
val groupedSymbols = completions.toList.groupBy(_.name.stripModuleClassSuffix.toSimpleName).toList
149+
val groupedSymbols = {
150+
val symbols = completions.toListWithNames
151+
val nameToSymbols = symbols.groupBy(_._2.stripModuleClassSuffix.toSimpleName)
152+
nameToSymbols.mapValues(_.map(_._1)).toList
153+
}
150154
groupedSymbols.map { case (name, symbols) =>
151155
val typesFirst = symbols.sortWith((s, _) => s.isType)
152156
// Use distinct to remove duplicates with class, module class, etc.
@@ -173,11 +177,11 @@ object Completion {
173177
if (ctx.owner.isClass) {
174178
addAccessibleMembers(ctx.owner.thisType)
175179
ctx.owner.asClass.classInfo.selfInfo match {
176-
case selfSym: Symbol => add(selfSym)
180+
case selfSym: Symbol => add(selfSym, selfSym.name)
177181
case _ =>
178182
}
179183
}
180-
else if (ctx.scope != null) ctx.scope.foreach(add)
184+
else if (ctx.scope != null) ctx.scope.foreach(s => add(s, s.name))
181185

182186
addImportCompletions
183187

@@ -208,15 +212,16 @@ object Completion {
208212
* If `sym` exists, no symbol with the same name is already included, and it satisfies the
209213
* inclusion filter, then add it to the completions.
210214
*/
211-
private def add(sym: Symbol)(implicit ctx: Context) =
212-
if (sym.exists && !completions.lookup(sym.name).exists && include(sym)) {
213-
completions.enter(sym)
215+
private def add(sym: Symbol, nameInScope: Name)(implicit ctx: Context) =
216+
if (sym.exists && !completions.lookup(nameInScope).exists && include(sym, nameInScope)) {
217+
completions.enter(sym, nameInScope)
214218
}
215219

216220
/** Lookup members `name` from `site`, and try to add them to the completion list. */
217-
private def addMember(site: Type, name: Name)(implicit ctx: Context) =
218-
if (!completions.lookup(name).exists)
219-
for (alt <- site.member(name).alternatives) add(alt.symbol)
221+
private def addMember(site: Type, name: Name, nameInScope: Name)(implicit ctx: Context) =
222+
if (!completions.lookup(nameInScope).exists) {
223+
for (alt <- site.member(name).alternatives) add(alt.symbol, nameInScope)
224+
}
220225

221226
/** Include in completion sets only symbols that
222227
* 1. start with given name prefix, and
@@ -230,9 +235,9 @@ object Completion {
230235
* as completion results. However, if a user explicitly writes all '$' characters in an
231236
* identifier, we should complete the rest.
232237
*/
233-
private def include(sym: Symbol)(implicit ctx: Context): Boolean =
234-
sym.name.startsWith(prefix) &&
235-
!sym.name.toString.drop(prefix.length).contains('$') &&
238+
private def include(sym: Symbol, nameInScope: Name)(implicit ctx: Context): Boolean =
239+
nameInScope.startsWith(prefix) &&
240+
!nameInScope.toString.drop(prefix.length).contains('$') &&
236241
!sym.isPrimaryConstructor &&
237242
(!sym.is(Package) || !sym.moduleClass.exists) &&
238243
!sym.is(allOf(Mutable, Accessor)) &&
@@ -249,20 +254,20 @@ object Completion {
249254
*/
250255
private def accessibleMembers(site: Type)(implicit ctx: Context): Seq[Symbol] = site match {
251256
case site: NamedType if site.symbol.is(Package) =>
252-
site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive.
257+
site.decls.toList.filter(sym => include(sym, sym.name)) // Don't look inside package members -- it's too expensive.
253258
case _ =>
254259
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
255260
try buf ++= site.member(name).alternatives
256261
catch { case ex: TypeError => }
257262
site.memberDenots(takeAllFilter, appendMemberSyms).collect {
258-
case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess = true).symbol
263+
case mbr if include(mbr.symbol, mbr.symbol.name) => mbr.accessibleFrom(site, superAccess = true).symbol
259264
case _ => NoSymbol
260265
}.filter(_.exists)
261266
}
262267

263268
/** Add all the accessible members of `site` in `info`. */
264269
private def addAccessibleMembers(site: Type)(implicit ctx: Context): Unit =
265-
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name)
270+
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name, mbr.name)
266271

267272
/**
268273
* Add in `info` the symbols that are imported by `ctx.importInfo`. If this is a wildcard import,
@@ -271,17 +276,18 @@ object Completion {
271276
private def addImportCompletions(implicit ctx: Context): Unit = {
272277
val imp = ctx.importInfo
273278
if (imp != null) {
274-
def addImport(name: TermName) = {
275-
addMember(imp.site, name)
276-
addMember(imp.site, name.toTypeName)
279+
def addImport(name: TermName, nameInScope: TermName) = {
280+
addMember(imp.site, name, nameInScope)
281+
addMember(imp.site, name.toTypeName, nameInScope.toTypeName)
282+
}
283+
imp.reverseMapping.foreachBinding { (nameInScope, original) =>
284+
if (original != nameInScope || !imp.excluded.contains(original)) {
285+
addImport(original, nameInScope)
286+
}
277287
}
278-
// FIXME: We need to also take renamed items into account for completions,
279-
// That means we have to return list of a pairs (Name, Symbol) instead of a list
280-
// of symbols from `completions`.!=
281-
for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported)
282288
if (imp.isWildcardImport)
283289
for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName))
284-
addMember(imp.site, mbr.name)
290+
addMember(imp.site, mbr.name, mbr.name)
285291
}
286292
}
287293

@@ -324,4 +330,23 @@ object Completion {
324330
val Import: Mode = new Mode(4) | Term | Type
325331
}
326332

333+
/** A scope that tracks renames of the entered symbols.
334+
* Useful for providing completions for renamed symbols
335+
* in the REPL and the IDE.
336+
*/
337+
private class RenameAwareScope extends Scopes.MutableScope {
338+
private[this] val renames: mutable.Map[Symbol, Name] = mutable.Map.empty
339+
340+
/** Enter the symbol `sym` in this scope, recording a potential renaming. */
341+
def enter[T <: Symbol](sym: T, name: Name)(implicit ctx: Context): T = {
342+
if (name != sym.name) renames += sym -> name
343+
newScopeEntry(name, sym)
344+
sym
345+
}
346+
347+
/** Lists the symbols in this scope along with the name associated with them. */
348+
def toListWithNames(implicit ctx: Context): List[(Symbol, Name)] =
349+
toList.map(sym => (sym, renames.get(sym).getOrElse(sym.name)))
350+
}
351+
327352
}

compiler/test/dotty/tools/repl/TabcompleteTests.scala

+10
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,14 @@ class TabcompleteTests extends ReplTest {
8080
val expected = List("FileDescriptor")
8181
assertEquals(expected, tabComplete("val foo: FileDesc"))
8282
}
83+
84+
@Test def tabCompleteRenamedImport =
85+
fromInitialState { implicit state =>
86+
val src = "import java.io.{FileDescriptor => Renamed}"
87+
run(src)
88+
}
89+
.andThen { implicit state =>
90+
val expected = List("Renamed")
91+
assertEquals(expected, tabComplete("val foo: Rena"))
92+
}
8393
}

language-server/test/dotty/tools/languageserver/CompletionTest.scala

+15
Original file line numberDiff line numberDiff line change
@@ -199,4 +199,19 @@ class CompletionTest {
199199
|}""".withSource
200200
.completion(m1, Set(("bar", Field, "Bar"), ("bat", Module, "Foo.bat")))
201201
}
202+
203+
@Test def completionOnRenamedImport: Unit = {
204+
code"""import java.io.{FileDescriptor => AwesomeStuff}
205+
trait Foo { val x: Awesom$m1 }""".withSource
206+
.completion(m1, Set(("AwesomeStuff", Class, "java.io.FileDescriptor")))
207+
}
208+
209+
@Test def completionOnRenamedImport2: Unit = {
210+
code"""import java.util.{HashMap => MyImportedSymbol}
211+
trait Foo {
212+
import java.io.{FileDescriptor => MyImportedSymbol}
213+
val x: MyImp$m1
214+
}""".withSource
215+
.completion(m1, Set(("MyImportedSymbol", Class, "java.io.FileDescriptor")))
216+
}
202217
}

0 commit comments

Comments
 (0)