Skip to content

Commit e33c635

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 2d23cba commit e33c635

File tree

5 files changed

+82
-32
lines changed

5 files changed

+82
-32
lines changed

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

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -156,15 +156,15 @@ object Interactive {
156156

157157
/** Get possible completions from tree at `pos`
158158
*
159-
* @return offset and list of symbols for possible completions
159+
* @return offset and list of (symbol, name in scope) for possible completions
160160
*/
161-
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = {
161+
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[(Symbol, Name)]) = {
162162
val path = pathTo(ctx.compilationUnit.tpdTree, pos.pos)
163163
computeCompletions(pos, path)(contextOfPath(path))
164164
}
165165

166-
private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Symbol]) = {
167-
val completions = Scopes.newScope.openForMutations
166+
private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[(Symbol, Name)]) = {
167+
val completions = new RenameAwareScope
168168

169169
val (completionPos, prefix, termOnly, typeOnly) = path match {
170170
case (ref: RefTree) :: _ =>
@@ -188,53 +188,53 @@ object Interactive {
188188
* as completion results. However, if a user explicitly writes all '$' characters in an
189189
* identifier, we should complete the rest.
190190
*/
191-
def include(sym: Symbol) =
192-
sym.name.startsWith(prefix) &&
193-
!sym.name.toString.drop(prefix.length).contains('$') &&
191+
def include(sym: Symbol, nameInScope: Name) =
192+
nameInScope.startsWith(prefix) &&
193+
!nameInScope.toString.drop(prefix.length).contains('$') &&
194194
(!termOnly || sym.isTerm) &&
195195
(!typeOnly || sym.isType)
196196

197-
def enter(sym: Symbol) =
198-
if (include(sym)) completions.enter(sym)
197+
def enter(sym: Symbol, nameInScope: Name) =
198+
if (include(sym, nameInScope)) completions.enter(sym, nameInScope)
199199

200200
def add(sym: Symbol) =
201-
if (sym.exists && !completions.lookup(sym.name).exists) enter(sym)
201+
if (sym.exists && !completions.lookup(sym.name).exists) enter(sym, sym.name)
202202

203-
def addMember(site: Type, name: Name) =
204-
if (!completions.lookup(name).exists)
205-
for (alt <- site.member(name).alternatives) enter(alt.symbol)
203+
def addMember(site: Type, name: Name, nameInScope: Name) =
204+
if (!completions.lookup(nameInScope).exists)
205+
for (alt <- site.member(name).alternatives) enter(alt.symbol, nameInScope)
206206

207207
def accessibleMembers(site: Type, superAccess: Boolean = true): Seq[Symbol] = site match {
208208
case site: NamedType if site.symbol.is(Package) =>
209-
site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive.
209+
site.decls.toList.filter(sym => include(sym, sym.name)) // Don't look inside package members -- it's too expensive.
210210
case _ =>
211211
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
212212
try buf ++= site.member(name).alternatives
213213
catch { case ex: TypeError => }
214214
site.memberDenots(takeAllFilter, appendMemberSyms).collect {
215-
case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess).symbol
215+
case mbr if include(mbr.symbol, mbr.symbol.name) => mbr.accessibleFrom(site, superAccess).symbol
216216
case _ => NoSymbol
217217
}.filter(_.exists)
218218
}
219219

220220
def addAccessibleMembers(site: Type, superAccess: Boolean = true): Unit =
221-
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name)
221+
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name, mbr.name)
222222

223223
def getImportCompletions(ictx: Context): Unit = {
224224
implicit val ctx = ictx
225225
val imp = ctx.importInfo
226226
if (imp != null) {
227-
def addImport(name: TermName) = {
228-
addMember(imp.site, name)
229-
addMember(imp.site, name.toTypeName)
227+
def addImport(original: TermName, nameInScope: TermName) = {
228+
addMember(imp.site, original, nameInScope)
229+
addMember(imp.site, original.toTypeName, nameInScope.toTypeName)
230+
}
231+
imp.reverseMapping.foreachBinding { (nameInScope, original) =>
232+
if (original != nameInScope || !imp.excluded.contains(original))
233+
addImport(original, nameInScope)
230234
}
231-
// FIXME: We need to also take renamed items into account for completions,
232-
// That means we have to return list of a pairs (Name, Symbol) instead of a list
233-
// of symbols from `completions`.!=
234-
for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported)
235235
if (imp.isWildcardImport)
236236
for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName))
237-
addMember(imp.site, mbr.name)
237+
addMember(imp.site, mbr.name, mbr.name)
238238
}
239239
}
240240

@@ -279,7 +279,7 @@ object Interactive {
279279
case _ => getScopeCompletions(ctx)
280280
}
281281

282-
val completionList = completions.toList
282+
val completionList = completions.toListWithNames
283283
interactiv.println(i"completion with pos = $pos, prefix = $prefix, termOnly = $termOnly, typeOnly = $typeOnly = $completionList%, %")
284284
(completionPos, completionList)
285285
}
@@ -293,7 +293,7 @@ object Interactive {
293293
def addMember(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
294294
buf ++= prefix.member(name).altsWith(sym =>
295295
!exclude(sym) && sym.isAccessibleFrom(prefix)(boundaryCtx))
296-
prefix.memberDenots(completionsFilter, addMember).map(_.symbol).toList
296+
prefix.memberDenots(completionsFilter, addMember).map(_.symbol).toList
297297
}
298298
else Nil
299299
}
@@ -572,4 +572,23 @@ object Interactive {
572572
n0.stripModuleClassSuffix.toTermName eq n1.stripModuleClassSuffix.toTermName
573573
}
574574

575+
/** A scope that tracks renames of the entered symbols.
576+
* Useful for providing completions for renamed symbols
577+
* in the REPL and the IDE.
578+
*/
579+
private class RenameAwareScope extends Scopes.MutableScope {
580+
private[this] val renames: mutable.Map[Symbol, Name] = mutable.Map.empty
581+
582+
/** Enter the symbol `sym` in this scope, recording a potential renaming. */
583+
def enter[T <: Symbol](sym: T, name: Name)(implicit ctx: Context): T = {
584+
if (name != sym.name) renames += sym -> name
585+
newScopeEntry(name, sym)
586+
sym
587+
}
588+
589+
/** Lists the symbols in this scope along with the name associated with them. */
590+
def toListWithNames(implicit ctx: Context): List[(Symbol, Name)] =
591+
toList.map(sym => (sym, renames.get(sym).getOrElse(sym.name)))
592+
}
593+
575594
}

compiler/src/dotty/tools/repl/ReplDriver.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ class ReplDriver(settings: Array[String],
149149

150150
/** Extract possible completions at the index of `cursor` in `expr` */
151151
protected[this] final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = {
152-
def makeCandidate(completion: Symbol)(implicit ctx: Context) = {
153-
val displ = completion.name.toString
152+
def makeCandidate(name: Name)(implicit ctx: Context) = {
153+
val displ = name.toString
154154
new Candidate(
155155
/* value = */ displ,
156156
/* displ = */ displ, // displayed value
@@ -171,7 +171,7 @@ class ReplDriver(settings: Array[String],
171171
implicit val ctx = state.context.fresh.setCompilationUnit(unit)
172172
val srcPos = SourcePosition(file, Position(cursor))
173173
val (_, completions) = Interactive.completions(srcPos)
174-
completions.map(makeCandidate)
174+
completions.map(c => makeCandidate(c._2))
175175
}
176176
.getOrElse(Nil)
177177
}

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

Lines changed: 10 additions & 0 deletions
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/src/dotty/tools/languageserver/DottyLanguageServer.scala

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -285,7 +285,7 @@ class DottyLanguageServer extends LanguageServer
285285
}
286286

287287
JEither.forRight(new CompletionList(
288-
/*isIncomplete = */ false, items.map(completionItem).asJava))
288+
/*isIncomplete = */ false, items.map(completionItem.tupled).asJava))
289289
}
290290

291291
/** If cursor is on a reference, show its definition and all overriding definitions in
@@ -774,7 +774,7 @@ object DottyLanguageServer {
774774
}
775775

776776
/** Create an lsp4j.CompletionItem from a Symbol */
777-
def completionItem(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItem = {
777+
def completionItem(sym: Symbol, name: Name)(implicit ctx: Context): lsp4j.CompletionItem = {
778778
def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = {
779779
import lsp4j.{CompletionItemKind => CIK}
780780

@@ -792,7 +792,7 @@ object DottyLanguageServer {
792792
CIK.Field
793793
}
794794

795-
val label = sym.name.show
795+
val label = name.show
796796
val item = new lsp4j.CompletionItem(label)
797797
item.setDetail(sym.info.widenTermRefExpr.show)
798798
item.setKind(completionItemKind(sym))

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,25 @@ class CompletionTest {
1919
code"object Main { import Foo._; val bar: Bar = new Bar; bar.b${m1} }"
2020
) .completion(m1, Set(("baz", CompletionItemKind.Method, "=> Int")))
2121
}
22+
23+
@Test def completionOnImport: Unit = {
24+
code"""import java.io.FileDescriptor
25+
trait Foo { val x: FileDesc$m1 }""".withSource
26+
.completion(m1, Set(("FileDescriptor", CompletionItemKind.Class, "Object{...}")))
27+
}
28+
29+
@Test def completionOnRenamedImport: Unit = {
30+
code"""import java.io.{FileDescriptor => AwesomeStuff}
31+
trait Foo { val x: Awesom$m1 }""".withSource
32+
.completion(m1, Set(("AwesomeStuff", CompletionItemKind.Class, "Object{...}")))
33+
}
34+
35+
@Test def completionOnRenamedImport2: Unit = {
36+
code"""import java.util.{HashMap => MyImportedSymbol}
37+
trait Foo {
38+
import java.io.{FileDescriptor => MyImportedSymbol}
39+
val x: MyImp$m1
40+
}""".withSource
41+
.completion(m1, Set(("MyImportedSymbol", CompletionItemKind.Class, "Object{...}")))
42+
}
2243
}

0 commit comments

Comments
 (0)