|
| 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