Skip to content

Commit 7c7af27

Browse files
authored
Merge pull request #4977 from dotty-staging/fix/3979
Fix #3979: Completion for renamed imports
2 parents ee8ff7f + 29fe495 commit 7c7af27

File tree

6 files changed

+192
-58
lines changed

6 files changed

+192
-58
lines changed

compiler/src/dotty/tools/dotc/core/Definitions.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ class Definitions {
285285
// technique to do that. Here we need to set it before completing
286286
// attempt to load Object's classfile, which causes issue #1648.
287287
val companion = JavaLangPackageVal.info.decl(nme.Object).symbol
288+
companion.moduleClass.info = NoType // to indicate that it does not really exist
288289
companion.info = NoType // to indicate that it does not really exist
289290

290291
completeClass(cls)

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

Lines changed: 106 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,23 @@ import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol}
1414
import dotty.tools.dotc.core.Scopes
1515
import dotty.tools.dotc.core.StdNames.{nme, tpnme}
1616
import dotty.tools.dotc.core.TypeError
17-
import dotty.tools.dotc.core.Types.{NamedType, Type, takeAllFilter}
17+
import dotty.tools.dotc.core.Types.{NameFilter, NamedType, Type, NoType}
1818
import dotty.tools.dotc.printing.Texts._
1919
import dotty.tools.dotc.util.{NoSourcePosition, SourcePosition}
2020

2121
import scala.collection.mutable
2222

23+
/**
24+
* One of the results of a completion query.
25+
*
26+
* @param label The label of this completion result, or the text that this completion result
27+
* should insert in the scope where the completion request happened.
28+
* @param description The description of this completion result: the fully qualified name for
29+
* types, or the type for terms.
30+
* @param symbols The symbols that are matched by this completion result.
31+
*/
32+
case class Completion(label: String, description: String, symbols: List[Symbol])
33+
2334
object Completion {
2435

2536
import dotty.tools.dotc.ast.tpd._
@@ -28,7 +39,7 @@ object Completion {
2839
*
2940
* @return offset and list of symbols for possible completions
3041
*/
31-
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = {
42+
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Completion]) = {
3243
val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.pos)
3344
computeCompletions(pos, path)(Interactive.contextOfPath(path))
3445
}
@@ -100,7 +111,7 @@ object Completion {
100111
new CompletionBuffer(mode, prefix, pos)
101112
}
102113

103-
private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Symbol]) = {
114+
private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Completion]) = {
104115

105116
val offset = completionOffset(path)
106117
val buffer = completionBuffer(path, pos)
@@ -126,20 +137,43 @@ object Completion {
126137

127138
private class CompletionBuffer(val mode: Mode, val prefix: String, pos: SourcePosition) {
128139

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

131142
/**
132143
* Return the list of symbols that shoudl be included in completion results.
133144
*
134-
* If the mode is `Import` and several symbols share the same name, the type symbols are
135-
* preferred over term symbols.
145+
* If several symbols share the same name, the type symbols appear before term symbols inside
146+
* the same `Completion`.
147+
*/
148+
def getCompletions(implicit ctx: Context): List[Completion] = {
149+
val nameToSymbols = completions.mappings.toList
150+
nameToSymbols.map { case (name, symbols) =>
151+
val typesFirst = symbols.sortWith((s1, s2) => s1.isType && !s2.isType)
152+
val desc = description(typesFirst)
153+
Completion(name.toString, desc, typesFirst)
154+
}
155+
}
156+
157+
/**
158+
* A description for completion result that represents `symbols`.
159+
*
160+
* If `symbols` contains a single symbol, show its full name in case it's a type, or its type if
161+
* it's a term.
162+
*
163+
* When there are multiple symbols, show their kinds.
136164
*/
137-
def getCompletions(implicit ctx: Context): List[Symbol] = {
138-
// Show only the type symbols when there are multiple options with the same name
139-
completions.toList.groupBy(_.name.stripModuleClassSuffix.toSimpleName).mapValues {
140-
case sym :: Nil => sym :: Nil
141-
case syms => syms.filter(_.isType)
142-
}.values.flatten.toList
165+
private def description(symbols: List[Symbol])(implicit ctx: Context): String = {
166+
symbols match {
167+
case sym :: Nil =>
168+
if (sym.isType) sym.showFullName
169+
else sym.info.widenTermRefExpr.show
170+
171+
case sym :: _ =>
172+
symbols.map(ctx.printer.kindString).mkString("", " and ", s" ${sym.name.show}")
173+
174+
case Nil =>
175+
""
176+
}
143177
}
144178

145179
/**
@@ -150,11 +184,11 @@ object Completion {
150184
if (ctx.owner.isClass) {
151185
addAccessibleMembers(ctx.owner.thisType)
152186
ctx.owner.asClass.classInfo.selfInfo match {
153-
case selfSym: Symbol => add(selfSym)
187+
case selfSym: Symbol => add(selfSym, selfSym.name)
154188
case _ =>
155189
}
156190
}
157-
else if (ctx.scope != null) ctx.scope.foreach(add)
191+
else if (ctx.scope != null) ctx.scope.foreach(s => add(s, s.name))
158192

159193
addImportCompletions
160194

@@ -185,32 +219,34 @@ object Completion {
185219
* If `sym` exists, no symbol with the same name is already included, and it satisfies the
186220
* inclusion filter, then add it to the completions.
187221
*/
188-
private def add(sym: Symbol)(implicit ctx: Context) =
189-
if (sym.exists && !completions.lookup(sym.name).exists && include(sym)) {
190-
completions.enter(sym)
222+
private def add(sym: Symbol, nameInScope: Name)(implicit ctx: Context) =
223+
if (sym.exists &&
224+
completionsFilter(NoType, nameInScope) &&
225+
!completions.lookup(nameInScope).exists &&
226+
include(sym, nameInScope)) {
227+
completions.enter(sym, nameInScope)
191228
}
192229

193230
/** Lookup members `name` from `site`, and try to add them to the completion list. */
194-
private def addMember(site: Type, name: Name)(implicit ctx: Context) =
195-
if (!completions.lookup(name).exists)
196-
for (alt <- site.member(name).alternatives) add(alt.symbol)
231+
private def addMember(site: Type, name: Name, nameInScope: Name)(implicit ctx: Context) =
232+
if (!completions.lookup(nameInScope).exists) {
233+
for (alt <- site.member(name).alternatives) add(alt.symbol, nameInScope)
234+
}
197235

198236
/** Include in completion sets only symbols that
199237
* 1. start with given name prefix, and
200-
* 2. do not contain '$' except in prefix where it is explicitly written by user, and
238+
* 2. is not absent (info is not NoType)
201239
* 3. are not a primary constructor,
202-
* 4. are the module class in case of packages,
203-
* 5. are mutable accessors, to exclude setters for `var`,
204-
* 6. have same term/type kind as name prefix given so far
205-
*
206-
* The reason for (2) is that we do not want to present compiler-synthesized identifiers
207-
* as completion results. However, if a user explicitly writes all '$' characters in an
208-
* identifier, we should complete the rest.
240+
* 4. have an existing source symbol,
241+
* 5. are the module class in case of packages,
242+
* 6. are mutable accessors, to exclude setters for `var`,
243+
* 7. have same term/type kind as name prefix given so far
209244
*/
210-
private def include(sym: Symbol)(implicit ctx: Context): Boolean =
211-
sym.name.startsWith(prefix) &&
212-
!sym.name.toString.drop(prefix.length).contains('$') &&
245+
private def include(sym: Symbol, nameInScope: Name)(implicit ctx: Context): Boolean =
246+
nameInScope.startsWith(prefix) &&
247+
!sym.isAbsent &&
213248
!sym.isPrimaryConstructor &&
249+
sym.sourceSymbol.exists &&
214250
(!sym.is(Package) || !sym.moduleClass.exists) &&
215251
!sym.is(allOf(Mutable, Accessor)) &&
216252
(
@@ -226,20 +262,21 @@ object Completion {
226262
*/
227263
private def accessibleMembers(site: Type)(implicit ctx: Context): Seq[Symbol] = site match {
228264
case site: NamedType if site.symbol.is(Package) =>
229-
site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive.
265+
// Don't look inside package members -- it's too expensive.
266+
site.decls.toList.filter(sym => sym.isAccessibleFrom(site, superAccess = false))
230267
case _ =>
231268
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
232269
try buf ++= site.member(name).alternatives
233270
catch { case ex: TypeError => }
234-
site.memberDenots(takeAllFilter, appendMemberSyms).collect {
235-
case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess = true).symbol
271+
site.memberDenots(completionsFilter, appendMemberSyms).collect {
272+
case mbr if include(mbr.symbol, mbr.symbol.name) => mbr.accessibleFrom(site, superAccess = true).symbol
236273
case _ => NoSymbol
237274
}.filter(_.exists)
238275
}
239276

240277
/** Add all the accessible members of `site` in `info`. */
241278
private def addAccessibleMembers(site: Type)(implicit ctx: Context): Unit =
242-
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name)
279+
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name, mbr.name)
243280

244281
/**
245282
* Add in `info` the symbols that are imported by `ctx.importInfo`. If this is a wildcard import,
@@ -248,17 +285,18 @@ object Completion {
248285
private def addImportCompletions(implicit ctx: Context): Unit = {
249286
val imp = ctx.importInfo
250287
if (imp != null) {
251-
def addImport(name: TermName) = {
252-
addMember(imp.site, name)
253-
addMember(imp.site, name.toTypeName)
288+
def addImport(name: TermName, nameInScope: TermName) = {
289+
addMember(imp.site, name, nameInScope)
290+
addMember(imp.site, name.toTypeName, nameInScope.toTypeName)
291+
}
292+
imp.reverseMapping.foreachBinding { (nameInScope, original) =>
293+
if (original != nameInScope || !imp.excluded.contains(original)) {
294+
addImport(original, nameInScope)
295+
}
254296
}
255-
// FIXME: We need to also take renamed items into account for completions,
256-
// That means we have to return list of a pairs (Name, Symbol) instead of a list
257-
// of symbols from `completions`.!=
258-
for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported)
259297
if (imp.isWildcardImport)
260298
for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName))
261-
addMember(imp.site, mbr.name)
299+
addMember(imp.site, mbr.name, mbr.name)
262300
}
263301
}
264302

@@ -277,6 +315,12 @@ object Completion {
277315
targets
278316
}
279317

318+
/** Filter for names that should appear when looking for completions. */
319+
private[this] object completionsFilter extends NameFilter {
320+
def apply(pre: Type, name: Name)(implicit ctx: Context): Boolean =
321+
!name.isConstructorName && name.toTermName.info.kind == SimpleNameKind
322+
}
323+
280324
}
281325

282326
/**
@@ -301,4 +345,23 @@ object Completion {
301345
val Import: Mode = new Mode(4) | Term | Type
302346
}
303347

348+
/** A scope that tracks renames of the entered symbols.
349+
* Useful for providing completions for renamed symbols
350+
* in the REPL and the IDE.
351+
*/
352+
private class RenameAwareScope extends Scopes.MutableScope {
353+
private[this] val nameToSymbols: mutable.Map[TermName, List[Symbol]] = mutable.Map.empty
354+
355+
/** Enter the symbol `sym` in this scope, recording a potential renaming. */
356+
def enter[T <: Symbol](sym: T, name: Name)(implicit ctx: Context): T = {
357+
val termName = name.stripModuleClassSuffix.toTermName
358+
nameToSymbols += termName -> (sym :: nameToSymbols.getOrElse(termName, Nil))
359+
newScopeEntry(name, sym)
360+
sym
361+
}
362+
363+
/** Get the names that are known in this scope, along with the list of symbols they refer to. */
364+
def mappings: Map[TermName, List[Symbol]] = nameToSymbols.toMap
365+
}
366+
304367
}

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

Lines changed: 2 additions & 2 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(completion: Completion)(implicit ctx: Context) = {
153+
val displ = completion.label
154154
new Candidate(
155155
/* value = */ displ,
156156
/* displ = */ displ, // displayed value

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: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -780,8 +780,8 @@ object DottyLanguageServer {
780780
symbol.owner == ctx.definitions.EmptyPackageClass
781781
}
782782

783-
/** Create an lsp4j.CompletionItem from a Symbol */
784-
def completionItem(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItem = {
783+
/** Create an lsp4j.CompletionItem from a completion result */
784+
def completionItem(completion: Completion)(implicit ctx: Context): lsp4j.CompletionItem = {
785785
def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = {
786786
import lsp4j.{CompletionItemKind => CIK}
787787

@@ -799,15 +799,20 @@ object DottyLanguageServer {
799799
CIK.Field
800800
}
801801

802-
val label = sym.name.show
803-
val item = new lsp4j.CompletionItem(label)
804-
val detail = if (sym.isType) sym.showFullName else sym.info.widenTermRefExpr.show
805-
item.setDetail(detail)
806-
ParsedComment.docOf(sym).foreach { doc =>
807-
item.setDocumentation(markupContent(doc.renderAsMarkdown))
802+
val item = new lsp4j.CompletionItem(completion.label)
803+
item.setDetail(completion.description)
804+
805+
val documentation = for {
806+
sym <- completion.symbols
807+
doc <- ParsedComment.docOf(sym)
808+
} yield doc
809+
810+
if (documentation.nonEmpty) {
811+
item.setDocumentation(hoverContent(None, documentation))
808812
}
809-
item.setDeprecated(sym.isDeprecated)
810-
item.setKind(completionItemKind(sym))
813+
814+
item.setDeprecated(completion.symbols.forall(_.isDeprecated))
815+
completion.symbols.headOption.foreach(s => item.setKind(completionItemKind(s)))
811816
item
812817
}
813818

0 commit comments

Comments
 (0)