Skip to content

Commit 1e82eb0

Browse files
committed
Refactor completions, add interactive.Completion
This commit moves all completion-related methods to their own file and cleans it up.
1 parent ae59176 commit 1e82eb0

File tree

4 files changed

+313
-236
lines changed

4 files changed

+313
-236
lines changed
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
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.Decorators.StringInterpolators
7+
import dotty.tools.dotc.core.Denotations.SingleDenotation
8+
import dotty.tools.dotc.core.Flags._
9+
import dotty.tools.dotc.core.Names.{Name, TermName}
10+
import dotty.tools.dotc.core.NameKinds.SimpleNameKind
11+
import dotty.tools.dotc.core.NameOps.NameDecorator
12+
import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol}
13+
import dotty.tools.dotc.core.Scopes
14+
import dotty.tools.dotc.core.StdNames.nme
15+
import dotty.tools.dotc.core.TypeError
16+
import dotty.tools.dotc.core.Types.{NamedType, NameFilter, Type, takeAllFilter}
17+
import dotty.tools.dotc.printing.Texts._
18+
import dotty.tools.dotc.util.{NoSourcePosition, SourcePosition}
19+
20+
import scala.collection.mutable
21+
22+
object Completion {
23+
24+
import dotty.tools.dotc.ast.tpd._
25+
26+
/** Get possible completions from tree at `pos`
27+
*
28+
* @return offset and list of symbols for possible completions
29+
*/
30+
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = {
31+
val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.pos)
32+
computeCompletions(pos, path)(Interactive.contextOfPath(path))
33+
}
34+
35+
/**
36+
* Inspect `path` to determine what kinds of symbols should be considered.
37+
*
38+
* If the path starts with:
39+
* - a `RefTree`, then accept symbols of the same kind as its name;
40+
* - a renaming import, and the cursor is on the renamee, accept both terms and types;
41+
* - an import, accept both terms and types;
42+
*
43+
* Otherwise, provide no completion suggestion.
44+
*/
45+
private def completionMode(path: List[Tree], pos: SourcePosition): Mode = {
46+
path match {
47+
case (ref: RefTree) :: _ =>
48+
if (ref.name == nme.ERROR) Mode.None
49+
else 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+
if (!mode.is(Mode.Import)) completions.toList
139+
else {
140+
// In imports, show only the type symbols when there are multiple options with the same name
141+
completions.toList.groupBy(_.name.stripModuleClassSuffix.toSimpleName).mapValues {
142+
case sym :: Nil => sym :: Nil
143+
case syms => syms.filter(_.isType)
144+
}.values.flatten.toList
145+
}
146+
}
147+
148+
/**
149+
* Add symbols that are currently in scope to `info`: the members of the current class and the
150+
* symbols that have been imported.
151+
*/
152+
def addScopeCompletions(implicit ctx: Context): Unit = {
153+
if (ctx.owner.isClass) {
154+
addAccessibleMembers(ctx.owner.thisType)
155+
ctx.owner.asClass.classInfo.selfInfo match {
156+
case selfSym: Symbol => add(selfSym)
157+
case _ =>
158+
}
159+
}
160+
else if (ctx.scope != null) ctx.scope.foreach(add)
161+
162+
addImportCompletions
163+
164+
var outer = ctx.outer
165+
while ((outer.owner `eq` ctx.owner) && (outer.scope `eq` ctx.scope)) {
166+
addImportCompletions(outer)
167+
outer = outer.outer
168+
}
169+
if (outer `ne` NoContext) addScopeCompletions(outer)
170+
}
171+
172+
/**
173+
* Find all the members of `qual` and add the ones that pass the include filters to `info`.
174+
*
175+
* If `info.mode` is `Import`, the members added via implicit conversion on `qual` are not
176+
* considered.
177+
*/
178+
def addMemberCompletions(qual: Tree)(implicit ctx: Context): Unit = {
179+
addAccessibleMembers(qual.tpe)
180+
if (!mode.is(Mode.Import)) {
181+
// Implicit conversions do not kick in when importing
182+
implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState())
183+
.foreach(addAccessibleMembers)
184+
}
185+
}
186+
187+
/**
188+
* If `sym` exists, no symbol with the same name is already included, and it satisfies the
189+
* inclusion filter, then add it to the completions.
190+
*/
191+
private def add(sym: Symbol)(implicit ctx: Context) =
192+
if (sym.exists && !completions.lookup(sym.name).exists && include(sym)) {
193+
completions.enter(sym)
194+
}
195+
196+
/** Lookup members `name` from `site`, and try to add them to the completion list. */
197+
private def addMember(site: Type, name: Name)(implicit ctx: Context) =
198+
if (!completions.lookup(name).exists)
199+
for (alt <- site.member(name).alternatives) add(alt.symbol)
200+
201+
/** Include in completion sets only symbols that
202+
* 1. start with given name prefix, and
203+
* 2. do not contain '$' except in prefix where it is explicitly written by user, and
204+
* 3. are not a primary constructor,
205+
* 4. are the module class in case of packages,
206+
* 5. are mutable accessors, to exclude setters for `var`,
207+
* 6. have same term/type kind as name prefix given so far
208+
*
209+
* The reason for (2) is that we do not want to present compiler-synthesized identifiers
210+
* as completion results. However, if a user explicitly writes all '$' characters in an
211+
* identifier, we should complete the rest.
212+
*/
213+
private def include(sym: Symbol)(implicit ctx: Context): Boolean =
214+
sym.name.startsWith(prefix) &&
215+
!sym.name.toString.drop(prefix.length).contains('$') &&
216+
!sym.isPrimaryConstructor &&
217+
(!sym.is(Package) || !sym.moduleClass.exists) &&
218+
!sym.is(allOf(Mutable, Accessor)) &&
219+
(
220+
(mode.is(Mode.Term) && sym.isTerm)
221+
|| (mode.is(Mode.Type) && sym.isType)
222+
)
223+
224+
/**
225+
* Find all the members of `site` that are accessible and which should be included in `info`.
226+
*
227+
* @param site The type to inspect.
228+
* @return The members of `site` that are accessible and pass the include filter of `info`.
229+
*/
230+
private def accessibleMembers(site: Type)(implicit ctx: Context): Seq[Symbol] = site match {
231+
case site: NamedType if site.symbol.is(Package) =>
232+
site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive.
233+
case _ =>
234+
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit =
235+
try buf ++= site.member(name).alternatives
236+
catch { case ex: TypeError => }
237+
site.memberDenots(takeAllFilter, appendMemberSyms).collect {
238+
case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess = true).symbol
239+
case _ => NoSymbol
240+
}.filter(_.exists)
241+
}
242+
243+
/** Add all the accessible members of `site` in `info`. */
244+
private def addAccessibleMembers(site: Type)(implicit ctx: Context): Unit =
245+
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name)
246+
247+
/**
248+
* Add in `info` the symbols that are imported by `ctx.importInfo`. If this is a wildcard import,
249+
* all the accessible members of the import's `site` are included.
250+
*/
251+
private def addImportCompletions(implicit ctx: Context): Unit = {
252+
val imp = ctx.importInfo
253+
if (imp != null) {
254+
def addImport(name: TermName) = {
255+
addMember(imp.site, name)
256+
addMember(imp.site, name.toTypeName)
257+
}
258+
// FIXME: We need to also take renamed items into account for completions,
259+
// That means we have to return list of a pairs (Name, Symbol) instead of a list
260+
// of symbols from `completions`.!=
261+
for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported)
262+
if (imp.isWildcardImport)
263+
for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName))
264+
addMember(imp.site, mbr.name)
265+
}
266+
}
267+
268+
/**
269+
* Given `qual` of type T, finds all the types S such that there exists an implicit conversion
270+
* from T to S.
271+
*
272+
* @param qual The argument to which the implicit conversion should be applied.
273+
* @return The set of types that `qual` can be converted to.
274+
*/
275+
private def implicitConversionTargets(qual: Tree)(implicit ctx: Context): Set[Type] = {
276+
val typer = ctx.typer
277+
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.pos).allImplicits
278+
val targets = conversions.map(_.widen.finalResultType)
279+
interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %")
280+
targets
281+
}
282+
283+
}
284+
285+
/**
286+
* The completion mode: defines what kinds of symbols should be included in the completion
287+
* results.
288+
*/
289+
private class Mode(val bits: Int) extends AnyVal {
290+
def is(other: Mode): Boolean = (bits & other.bits) == other.bits
291+
def |(other: Mode): Mode = new Mode(bits | other.bits)
292+
}
293+
private object Mode {
294+
/** No symbol should be included */
295+
val None: Mode = new Mode(0)
296+
297+
/** Term symbols are allowed */
298+
val Term: Mode = new Mode(1)
299+
300+
/** Type symbols are allowed */
301+
val Type: Mode = new Mode(2)
302+
303+
/** Both term and type symbols are allowed */
304+
val Import: Mode = Term | Type
305+
}
306+
307+
}

0 commit comments

Comments
 (0)