Skip to content

Commit 9a9aa31

Browse files
authored
Merge pull request #5476 from dotty-staging/fix/5460
Fix #5460: Improve completion of import nodes
2 parents 95dde14 + 89f850a commit 9a9aa31

File tree

7 files changed

+536
-176
lines changed

7 files changed

+536
-176
lines changed
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
package dotty.tools.dotc.interactive
2+
3+
import dotty.tools.dotc.ast.Trees._
4+
import dotty.tools.dotc.config.Printers.interactiv
5+
import dotty.tools.dotc.core.Contexts.{Context, NoContext}
6+
import dotty.tools.dotc.core.CheckRealizable
7+
import dotty.tools.dotc.core.Decorators.StringInterpolators
8+
import dotty.tools.dotc.core.Denotations.SingleDenotation
9+
import dotty.tools.dotc.core.Flags._
10+
import dotty.tools.dotc.core.Names.{Name, TermName}
11+
import dotty.tools.dotc.core.NameKinds.SimpleNameKind
12+
import dotty.tools.dotc.core.NameOps.NameDecorator
13+
import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol}
14+
import dotty.tools.dotc.core.Scopes
15+
import dotty.tools.dotc.core.StdNames.{nme, tpnme}
16+
import dotty.tools.dotc.core.TypeError
17+
import dotty.tools.dotc.core.Types.{NamedType, Type, takeAllFilter}
18+
import dotty.tools.dotc.printing.Texts._
19+
import dotty.tools.dotc.util.{NoSourcePosition, SourcePosition}
20+
21+
import scala.collection.mutable
22+
23+
object Completion {
24+
25+
import dotty.tools.dotc.ast.tpd._
26+
27+
/** Get possible completions from tree at `pos`
28+
*
29+
* @return offset and list of symbols for possible completions
30+
*/
31+
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = {
32+
val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.pos)
33+
computeCompletions(pos, path)(Interactive.contextOfPath(path))
34+
}
35+
36+
/**
37+
* Inspect `path` to determine what kinds of symbols should be considered.
38+
*
39+
* If the path starts with:
40+
* - a `RefTree`, then accept symbols of the same kind as its name;
41+
* - a renaming import, and the cursor is on the renamee, accept both terms and types;
42+
* - an import, accept both terms and types;
43+
*
44+
* Otherwise, provide no completion suggestion.
45+
*/
46+
private def completionMode(path: List[Tree], pos: SourcePosition): Mode = {
47+
path match {
48+
case (ref: RefTree) :: _ =>
49+
if (ref.name.isTermName) Mode.Term
50+
else if (ref.name.isTypeName) Mode.Type
51+
else Mode.None
52+
53+
case Thicket(name :: _ :: Nil) :: (_: Import) :: _ =>
54+
if (name.pos.contains(pos.pos)) Mode.Import
55+
else Mode.None // Can't help completing the renaming
56+
57+
case Import(_, _) :: _ =>
58+
Mode.Import
59+
60+
case _ =>
61+
Mode.None
62+
}
63+
}
64+
65+
/**
66+
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the
67+
* returned prefix should be considered.
68+
*/
69+
private def completionPrefix(path: List[Tree], pos: SourcePosition): String = {
70+
path match {
71+
case Thicket(name :: _ :: Nil) :: (_: Import) :: _ =>
72+
completionPrefix(name :: Nil, pos)
73+
74+
case Import(expr, selectors) :: _ =>
75+
selectors.find(_.pos.contains(pos.pos)).map { selector =>
76+
completionPrefix(selector.asInstanceOf[Tree] :: Nil, pos)
77+
}.getOrElse("")
78+
79+
case (ref: RefTree) :: _ =>
80+
if (ref.name == nme.ERROR) ""
81+
else ref.name.toString.take(pos.pos.point - ref.pos.point)
82+
83+
case _ =>
84+
""
85+
}
86+
}
87+
88+
/** Inspect `path` to determine the offset where the completion result should be inserted. */
89+
private def completionOffset(path: List[Tree]): Int = {
90+
path match {
91+
case (ref: RefTree) :: _ => ref.pos.point
92+
case _ => 0
93+
}
94+
}
95+
96+
/** Create a new `CompletionBuffer` for completing at `pos`. */
97+
private def completionBuffer(path: List[Tree], pos: SourcePosition): CompletionBuffer = {
98+
val mode = completionMode(path, pos)
99+
val prefix = completionPrefix(path, pos)
100+
new CompletionBuffer(mode, prefix, pos)
101+
}
102+
103+
private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Symbol]) = {
104+
105+
val offset = completionOffset(path)
106+
val buffer = completionBuffer(path, pos)
107+
108+
if (buffer.mode != Mode.None) {
109+
path match {
110+
case Select(qual, _) :: _ => buffer.addMemberCompletions(qual)
111+
case Import(expr, _) :: _ => buffer.addMemberCompletions(expr)
112+
case (_: Thicket) :: Import(expr, _) :: _ => buffer.addMemberCompletions(expr)
113+
case _ => buffer.addScopeCompletions
114+
}
115+
}
116+
117+
val completionList = buffer.getCompletions
118+
119+
interactiv.println(i"""completion with pos = $pos,
120+
| prefix = ${buffer.prefix},
121+
| term = ${buffer.mode.is(Mode.Term)},
122+
| type = ${buffer.mode.is(Mode.Type)}
123+
| results = $completionList%, %""")
124+
(offset, completionList)
125+
}
126+
127+
private class CompletionBuffer(val mode: Mode, val prefix: String, pos: SourcePosition) {
128+
129+
private[this] val completions = Scopes.newScope.openForMutations
130+
131+
/**
132+
* Return the list of symbols that shoudl be included in completion results.
133+
*
134+
* If the mode is `Import` and several symbols share the same name, the type symbols are
135+
* preferred over term symbols.
136+
*/
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
143+
}
144+
145+
/**
146+
* Add symbols that are currently in scope to `info`: the members of the current class and the
147+
* symbols that have been imported.
148+
*/
149+
def addScopeCompletions(implicit ctx: Context): Unit = {
150+
if (ctx.owner.isClass) {
151+
addAccessibleMembers(ctx.owner.thisType)
152+
ctx.owner.asClass.classInfo.selfInfo match {
153+
case selfSym: Symbol => add(selfSym)
154+
case _ =>
155+
}
156+
}
157+
else if (ctx.scope != null) ctx.scope.foreach(add)
158+
159+
addImportCompletions
160+
161+
var outer = ctx.outer
162+
while ((outer.owner `eq` ctx.owner) && (outer.scope `eq` ctx.scope)) {
163+
addImportCompletions(outer)
164+
outer = outer.outer
165+
}
166+
if (outer `ne` NoContext) addScopeCompletions(outer)
167+
}
168+
169+
/**
170+
* Find all the members of `qual` and add the ones that pass the include filters to `info`.
171+
*
172+
* If `info.mode` is `Import`, the members added via implicit conversion on `qual` are not
173+
* considered.
174+
*/
175+
def addMemberCompletions(qual: Tree)(implicit ctx: Context): Unit = {
176+
addAccessibleMembers(qual.tpe)
177+
if (!mode.is(Mode.Import)) {
178+
// Implicit conversions do not kick in when importing
179+
implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState())
180+
.foreach(addAccessibleMembers)
181+
}
182+
}
183+
184+
/**
185+
* If `sym` exists, no symbol with the same name is already included, and it satisfies the
186+
* inclusion filter, then add it to the completions.
187+
*/
188+
private def add(sym: Symbol)(implicit ctx: Context) =
189+
if (sym.exists && !completions.lookup(sym.name).exists && include(sym)) {
190+
completions.enter(sym)
191+
}
192+
193+
/** 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)
197+
198+
/** Include in completion sets only symbols that
199+
* 1. start with given name prefix, and
200+
* 2. do not contain '$' except in prefix where it is explicitly written by user, and
201+
* 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.
209+
*/
210+
private def include(sym: Symbol)(implicit ctx: Context): Boolean =
211+
sym.name.startsWith(prefix) &&
212+
!sym.name.toString.drop(prefix.length).contains('$') &&
213+
!sym.isPrimaryConstructor &&
214+
(!sym.is(Package) || !sym.moduleClass.exists) &&
215+
!sym.is(allOf(Mutable, Accessor)) &&
216+
(
217+
(mode.is(Mode.Term) && sym.isTerm)
218+
|| (mode.is(Mode.Type) && (sym.isType || sym.isStable))
219+
)
220+
221+
/**
222+
* Find all the members of `site` that are accessible and which should be included in `info`.
223+
*
224+
* @param site The type to inspect.
225+
* @return The members of `site` that are accessible and pass the include filter of `info`.
226+
*/
227+
private def accessibleMembers(site: Type)(implicit ctx: Context): Seq[Symbol] = site match {
228+
case site: NamedType if site.symbol.is(Package) =>
229+
site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive.
230+
case _ =>
231+
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
232+
try buf ++= site.member(name).alternatives
233+
catch { case ex: TypeError => }
234+
site.memberDenots(takeAllFilter, appendMemberSyms).collect {
235+
case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess = true).symbol
236+
case _ => NoSymbol
237+
}.filter(_.exists)
238+
}
239+
240+
/** Add all the accessible members of `site` in `info`. */
241+
private def addAccessibleMembers(site: Type)(implicit ctx: Context): Unit =
242+
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name)
243+
244+
/**
245+
* Add in `info` the symbols that are imported by `ctx.importInfo`. If this is a wildcard import,
246+
* all the accessible members of the import's `site` are included.
247+
*/
248+
private def addImportCompletions(implicit ctx: Context): Unit = {
249+
val imp = ctx.importInfo
250+
if (imp != null) {
251+
def addImport(name: TermName) = {
252+
addMember(imp.site, name)
253+
addMember(imp.site, name.toTypeName)
254+
}
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)
259+
if (imp.isWildcardImport)
260+
for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName))
261+
addMember(imp.site, mbr.name)
262+
}
263+
}
264+
265+
/**
266+
* Given `qual` of type T, finds all the types S such that there exists an implicit conversion
267+
* from T to S.
268+
*
269+
* @param qual The argument to which the implicit conversion should be applied.
270+
* @return The set of types that `qual` can be converted to.
271+
*/
272+
private def implicitConversionTargets(qual: Tree)(implicit ctx: Context): Set[Type] = {
273+
val typer = ctx.typer
274+
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.pos).allImplicits
275+
val targets = conversions.map(_.widen.finalResultType)
276+
interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %")
277+
targets
278+
}
279+
280+
}
281+
282+
/**
283+
* The completion mode: defines what kinds of symbols should be included in the completion
284+
* results.
285+
*/
286+
private class Mode(val bits: Int) extends AnyVal {
287+
def is(other: Mode): Boolean = (bits & other.bits) == other.bits
288+
def |(other: Mode): Mode = new Mode(bits | other.bits)
289+
}
290+
private object Mode {
291+
/** No symbol should be included */
292+
val None: Mode = new Mode(0)
293+
294+
/** Term symbols are allowed */
295+
val Term: Mode = new Mode(1)
296+
297+
/** Type and stable term symbols are allowed */
298+
val Type: Mode = new Mode(2)
299+
300+
/** Both term and type symbols are allowed */
301+
val Import: Mode = new Mode(4) | Term | Type
302+
}
303+
304+
}

0 commit comments

Comments
 (0)