Skip to content

Commit 0975d2c

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 da2a245 commit 0975d2c

File tree

4 files changed

+73
-32
lines changed

4 files changed

+73
-32
lines changed

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

+46-26
Original file line numberDiff line numberDiff line change
@@ -96,15 +96,15 @@ object Interactive {
9696

9797
/** Get possible completions from tree at `pos`
9898
*
99-
* @return offset and list of symbols for possible completions
99+
* @return offset and list of (symbol, name in scope) for possible completions
100100
*/
101-
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = {
101+
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[(Symbol, Name)]) = {
102102
val path = pathTo(ctx.compilationUnit.tpdTree, pos.pos)
103103
computeCompletions(pos, path)(contextOfPath(path))
104104
}
105105

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

109109
val (completionPos, prefix, termOnly, typeOnly) = path match {
110110
case (ref: RefTree) :: _ =>
@@ -128,53 +128,53 @@ object Interactive {
128128
* as completion results. However, if a user explicitly writes all '$' characters in an
129129
* identifier, we should complete the rest.
130130
*/
131-
def include(sym: Symbol) =
132-
sym.name.startsWith(prefix) &&
133-
!sym.name.toString.drop(prefix.length).contains('$') &&
131+
def include(sym: Symbol, nameInScope: Name) =
132+
nameInScope.startsWith(prefix) &&
133+
!nameInScope.toString.drop(prefix.length).contains('$') &&
134134
(!termOnly || sym.isTerm) &&
135135
(!typeOnly || sym.isType)
136136

137-
def enter(sym: Symbol) =
138-
if (include(sym)) completions.enter(sym)
137+
def enter(sym: Symbol, nameInScope: Name) =
138+
if (include(sym, nameInScope)) completions.enter(sym, nameInScope)
139139

140140
def add(sym: Symbol) =
141-
if (sym.exists && !completions.lookup(sym.name).exists) enter(sym)
141+
if (sym.exists && !completions.lookup(sym.name).exists) enter(sym, sym.name)
142142

143-
def addMember(site: Type, name: Name) =
144-
if (!completions.lookup(name).exists)
145-
for (alt <- site.member(name).alternatives) enter(alt.symbol)
143+
def addMember(site: Type, name: Name, nameInScope: Name) =
144+
if (!completions.lookup(nameInScope).exists)
145+
for (alt <- site.member(name).alternatives) enter(alt.symbol, nameInScope)
146146

147147
def accessibleMembers(site: Type, superAccess: Boolean = true): Seq[Symbol] = site match {
148148
case site: NamedType if site.symbol.is(Package) =>
149-
site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive.
149+
site.decls.toList.filter(sym => include(sym, sym.name)) // Don't look inside package members -- it's too expensive.
150150
case _ =>
151151
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
152152
try buf ++= site.member(name).alternatives
153153
catch { case ex: TypeError => }
154154
site.memberDenots(takeAllFilter, appendMemberSyms).collect {
155-
case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess).symbol
155+
case mbr if include(mbr.symbol, mbr.symbol.name) => mbr.accessibleFrom(site, superAccess).symbol
156156
case _ => NoSymbol
157157
}.filter(_.exists)
158158
}
159159

160160
def addAccessibleMembers(site: Type, superAccess: Boolean = true): Unit =
161-
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name)
161+
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name, mbr.name)
162162

163163
def getImportCompletions(ictx: Context): Unit = {
164164
implicit val ctx = ictx
165165
val imp = ctx.importInfo
166166
if (imp != null) {
167-
def addImport(name: TermName) = {
168-
addMember(imp.site, name)
169-
addMember(imp.site, name.toTypeName)
167+
def addImport(original: TermName, nameInScope: TermName) = {
168+
addMember(imp.site, original, nameInScope)
169+
addMember(imp.site, original.toTypeName, nameInScope.toTypeName)
170+
}
171+
imp.reverseMapping.foreachBinding { (nameInScope, original) =>
172+
if (original != nameInScope || !imp.excluded.contains(original))
173+
addImport(original, nameInScope)
170174
}
171-
// FIXME: We need to also take renamed items into account for completions,
172-
// That means we have to return list of a pairs (Name, Symbol) instead of a list
173-
// of symbols from `completions`.!=
174-
for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported)
175175
if (imp.isWildcardImport)
176176
for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName))
177-
addMember(imp.site, mbr.name)
177+
addMember(imp.site, mbr.name, mbr.name)
178178
}
179179
}
180180

@@ -219,7 +219,7 @@ object Interactive {
219219
case _ => getScopeCompletions(ctx)
220220
}
221221

222-
val completionList = completions.toList
222+
val completionList = completions.toListWithNames
223223
interactiv.println(i"completion with pos = $pos, prefix = $prefix, termOnly = $termOnly, typeOnly = $typeOnly = $completionList%, %")
224224
(completionPos, completionList)
225225
}
@@ -233,7 +233,7 @@ object Interactive {
233233
def addMember(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
234234
buf ++= prefix.member(name).altsWith(sym =>
235235
!exclude(sym) && sym.isAccessibleFrom(prefix)(boundaryCtx))
236-
prefix.memberDenots(completionsFilter, addMember).map(_.symbol).toList
236+
prefix.memberDenots(completionsFilter, addMember).map(_.symbol).toList
237237
}
238238
else Nil
239239
}
@@ -377,4 +377,24 @@ object Interactive {
377377
/** The first tree in the path that is a definition. */
378378
def enclosingDefinitionInPath(path: List[Tree])(implicit ctx: Context): Tree =
379379
path.find(_.isInstanceOf[DefTree]).getOrElse(EmptyTree)
380+
381+
/** A scope that tracks renames of the entered symbols.
382+
* Useful for providing completions for renamed symbols
383+
* in the REPL and the IDE.
384+
*/
385+
private class RenameAwareScope extends Scopes.MutableScope {
386+
private[this] val renames: mutable.Map[Symbol, Name] = mutable.Map.empty
387+
388+
/** Enter the symbol `sym` in this scope, recording a potential renaming. */
389+
def enter[T <: Symbol](sym: T, name: Name)(implicit ctx: Context): T = {
390+
if (name != sym.name) renames += sym -> name
391+
newScopeEntry(name, sym)
392+
sym
393+
}
394+
395+
/** Lists the symbols in this scope along with the name associated with them. */
396+
def toListWithNames(implicit ctx: Context): List[(Symbol, Name)] =
397+
toList.map(sym => (sym, renames.get(sym).getOrElse(sym.name)))
398+
}
399+
380400
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -146,8 +146,8 @@ class ReplDriver(settings: Array[String],
146146

147147
/** Extract possible completions at the index of `cursor` in `expr` */
148148
protected[this] final def completions(cursor: Int, expr: String, state0: State): List[Candidate] = {
149-
def makeCandidate(completion: Symbol)(implicit ctx: Context) = {
150-
val displ = completion.name.toString
149+
def makeCandidate(name: Name)(implicit ctx: Context) = {
150+
val displ = name.toString
151151
new Candidate(
152152
/* value = */ displ,
153153
/* displ = */ displ, // displayed value
@@ -168,7 +168,7 @@ class ReplDriver(settings: Array[String],
168168
implicit val ctx = state.context.fresh.setCompilationUnit(unit)
169169
val srcPos = SourcePosition(file, Position(cursor))
170170
val (_, completions) = Interactive.completions(srcPos)
171-
completions.map(makeCandidate)
171+
completions.map(c => makeCandidate(c._2))
172172
}
173173
.getOrElse(Nil)
174174
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ class DottyLanguageServer extends LanguageServer
234234
}
235235

236236
JEither.forRight(new CompletionList(
237-
/*isIncomplete = */ false, items.map(completionItem).asJava))
237+
/*isIncomplete = */ false, items.map(completionItem.tupled).asJava))
238238
}
239239

240240
/** If cursor is on a reference, show its definition and all overriding definitions in
@@ -441,7 +441,7 @@ object DottyLanguageServer {
441441
}
442442

443443
/** Create an lsp4j.CompletionItem from a Symbol */
444-
def completionItem(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItem = {
444+
def completionItem(sym: Symbol, name: Name)(implicit ctx: Context): lsp4j.CompletionItem = {
445445
def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = {
446446
import lsp4j.{CompletionItemKind => CIK}
447447

@@ -459,7 +459,7 @@ object DottyLanguageServer {
459459
CIK.Field
460460
}
461461

462-
val label = sym.name.show
462+
val label = name.show
463463
val item = new lsp4j.CompletionItem(label)
464464
item.setDetail(sym.info.widenTermRefExpr.show)
465465
item.setKind(completionItemKind(sym))

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

+21
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,25 @@ class CompletionTest {
1111
code"class Foo { val xyz: Int = 0; def y: Int = xy$m1 }".withSource
1212
.completion(m1, Set(("xyz", CompletionItemKind.Field, "Int")))
1313
}
14+
15+
@Test def completionOnImport: Unit = {
16+
code"""import java.io.FileDescriptor
17+
trait Foo { val x: FileDesc$m1 }""".withSource
18+
.completion(m1, Set(("FileDescriptor", CompletionItemKind.Class, "Object{...}")))
19+
}
20+
21+
@Test def completionOnRenamedImport: Unit = {
22+
code"""import java.io.{FileDescriptor => AwesomeStuff}
23+
trait Foo { val x: Awesom$m1 }""".withSource
24+
.completion(m1, Set(("AwesomeStuff", CompletionItemKind.Class, "Object{...}")))
25+
}
26+
27+
@Test def completionOnRenamedImport2: Unit = {
28+
code"""import java.util.{HashMap => MyImportedSymbol}
29+
trait Foo {
30+
import java.io.{FileDescriptor => MyImportedSymbol}
31+
val x: MyImp$m1
32+
}""".withSource
33+
.completion(m1, Set(("MyImportedSymbol", CompletionItemKind.Class, "Object{...}")))
34+
}
1435
}

0 commit comments

Comments
 (0)