-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Fix #5460: Improve completion of import nodes #5476
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 10 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
43455e4
Fix #5460: Improve completion of import nodes
Duhemm c60930a
Exclude Java module class in completion in imports
Duhemm 25dc9cb
Fix completion with renaming imports
Duhemm 18f4c82
Refactor `completionInfo`
Duhemm ffa4691
Prefer types over terms in import completions
Duhemm 341dc28
Improve display of completion results
Duhemm 82e84d2
Mark deprecated symbols as such in completion
Duhemm ae59176
Address review comments
Duhemm 1e82eb0
Refactor completions, add interactive.Completion
Duhemm ace5092
Fix completion without prefix
Duhemm 9321210
Extract name kind from `ERROR`
Duhemm 5677dfc
Show stable terms when completing types
Duhemm a746215
Address review comments
Duhemm 89f850a
Always remove duplicates in completion
Duhemm File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
307 changes: 307 additions & 0 deletions
307
compiler/src/dotty/tools/dotc/interactive/Completion.scala
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,307 @@ | ||
package dotty.tools.dotc.interactive | ||
|
||
import dotty.tools.dotc.ast.Trees._ | ||
import dotty.tools.dotc.config.Printers.interactiv | ||
import dotty.tools.dotc.core.Contexts.{Context, NoContext} | ||
import dotty.tools.dotc.core.Decorators.StringInterpolators | ||
import dotty.tools.dotc.core.Denotations.SingleDenotation | ||
import dotty.tools.dotc.core.Flags._ | ||
import dotty.tools.dotc.core.Names.{Name, TermName} | ||
import dotty.tools.dotc.core.NameKinds.SimpleNameKind | ||
import dotty.tools.dotc.core.NameOps.NameDecorator | ||
import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol} | ||
import dotty.tools.dotc.core.Scopes | ||
import dotty.tools.dotc.core.StdNames.nme | ||
import dotty.tools.dotc.core.TypeError | ||
import dotty.tools.dotc.core.Types.{NamedType, NameFilter, Type, takeAllFilter} | ||
import dotty.tools.dotc.printing.Texts._ | ||
import dotty.tools.dotc.util.{NoSourcePosition, SourcePosition} | ||
|
||
import scala.collection.mutable | ||
|
||
object Completion { | ||
|
||
import dotty.tools.dotc.ast.tpd._ | ||
|
||
/** Get possible completions from tree at `pos` | ||
* | ||
* @return offset and list of symbols for possible completions | ||
*/ | ||
def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = { | ||
val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.pos) | ||
computeCompletions(pos, path)(Interactive.contextOfPath(path)) | ||
} | ||
|
||
/** | ||
* Inspect `path` to determine what kinds of symbols should be considered. | ||
* | ||
* If the path starts with: | ||
* - a `RefTree`, then accept symbols of the same kind as its name; | ||
* - a renaming import, and the cursor is on the renamee, accept both terms and types; | ||
* - an import, accept both terms and types; | ||
* | ||
* Otherwise, provide no completion suggestion. | ||
*/ | ||
private def completionMode(path: List[Tree], pos: SourcePosition): Mode = { | ||
path match { | ||
case (ref: RefTree) :: _ => | ||
if (ref.name == nme.ERROR) Mode.Term | Mode.Type | ||
else if (ref.name.isTermName) Mode.Term | ||
else if (ref.name.isTypeName) Mode.Type | ||
else Mode.None | ||
|
||
case Thicket(name :: _ :: Nil) :: (_: Import) :: _ => | ||
if (name.pos.contains(pos.pos)) Mode.Import | ||
else Mode.None // Can't help completing the renaming | ||
|
||
case Import(_, _) :: _ => | ||
Mode.Import | ||
|
||
case _ => | ||
Mode.None | ||
} | ||
} | ||
|
||
/** | ||
* Inspect `path` to determine the completion prefix. Only symbols whose name start with the | ||
* returned prefix should be considered. | ||
*/ | ||
private def completionPrefix(path: List[Tree], pos: SourcePosition): String = { | ||
path match { | ||
case Thicket(name :: _ :: Nil) :: (_: Import) :: _ => | ||
completionPrefix(name :: Nil, pos) | ||
|
||
case Import(expr, selectors) :: _ => | ||
selectors.find(_.pos.contains(pos.pos)).map { selector => | ||
completionPrefix(selector.asInstanceOf[Tree] :: Nil, pos) | ||
}.getOrElse("") | ||
|
||
case (ref: RefTree) :: _ => | ||
if (ref.name == nme.ERROR) "" | ||
else ref.name.toString.take(pos.pos.point - ref.pos.point) | ||
|
||
case _ => | ||
"" | ||
} | ||
} | ||
|
||
/** Inspect `path` to determine the offset where the completion result should be inserted. */ | ||
private def completionOffset(path: List[Tree]): Int = { | ||
path match { | ||
case (ref: RefTree) :: _ => ref.pos.point | ||
case _ => 0 | ||
} | ||
} | ||
|
||
/** Create a new `CompletionBuffer` for completing at `pos`. */ | ||
private def completionBuffer(path: List[Tree], pos: SourcePosition): CompletionBuffer = { | ||
val mode = completionMode(path, pos) | ||
val prefix = completionPrefix(path, pos) | ||
new CompletionBuffer(mode, prefix, pos) | ||
} | ||
|
||
private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Symbol]) = { | ||
|
||
val offset = completionOffset(path) | ||
val buffer = completionBuffer(path, pos) | ||
|
||
if (buffer.mode != Mode.None) { | ||
path match { | ||
case Select(qual, _) :: _ => buffer.addMemberCompletions(qual) | ||
case Import(expr, _) :: _ => buffer.addMemberCompletions(expr) | ||
case (_: Thicket) :: Import(expr, _) :: _ => buffer.addMemberCompletions(expr) | ||
case _ => buffer.addScopeCompletions | ||
} | ||
} | ||
|
||
val completionList = buffer.getCompletions | ||
|
||
interactiv.println(i"""completion with pos = $pos, | ||
| prefix = ${buffer.prefix}, | ||
| term = ${buffer.mode.is(Mode.Term)}, | ||
| type = ${buffer.mode.is(Mode.Type)} | ||
| results = $completionList%, %""") | ||
(offset, completionList) | ||
} | ||
|
||
private class CompletionBuffer(val mode: Mode, val prefix: String, pos: SourcePosition) { | ||
|
||
private[this] val completions = Scopes.newScope.openForMutations | ||
|
||
/** | ||
* Return the list of symbols that shoudl be included in completion results. | ||
* | ||
* If the mode is `Import` and several symbols share the same name, the type symbols are | ||
* preferred over term symbols. | ||
*/ | ||
def getCompletions(implicit ctx: Context): List[Symbol] = { | ||
if (!mode.is(Mode.Import)) completions.toList | ||
else { | ||
// In imports, show only the type symbols when there are multiple options with the same name | ||
completions.toList.groupBy(_.name.stripModuleClassSuffix.toSimpleName).mapValues { | ||
case sym :: Nil => sym :: Nil | ||
case syms => syms.filter(_.isType) | ||
}.values.flatten.toList | ||
} | ||
} | ||
|
||
/** | ||
* Add symbols that are currently in scope to `info`: the members of the current class and the | ||
* symbols that have been imported. | ||
*/ | ||
def addScopeCompletions(implicit ctx: Context): Unit = { | ||
if (ctx.owner.isClass) { | ||
addAccessibleMembers(ctx.owner.thisType) | ||
ctx.owner.asClass.classInfo.selfInfo match { | ||
case selfSym: Symbol => add(selfSym) | ||
case _ => | ||
} | ||
} | ||
else if (ctx.scope != null) ctx.scope.foreach(add) | ||
|
||
addImportCompletions | ||
|
||
var outer = ctx.outer | ||
while ((outer.owner `eq` ctx.owner) && (outer.scope `eq` ctx.scope)) { | ||
addImportCompletions(outer) | ||
outer = outer.outer | ||
} | ||
if (outer `ne` NoContext) addScopeCompletions(outer) | ||
} | ||
|
||
/** | ||
* Find all the members of `qual` and add the ones that pass the include filters to `info`. | ||
* | ||
* If `info.mode` is `Import`, the members added via implicit conversion on `qual` are not | ||
* considered. | ||
*/ | ||
def addMemberCompletions(qual: Tree)(implicit ctx: Context): Unit = { | ||
addAccessibleMembers(qual.tpe) | ||
if (!mode.is(Mode.Import)) { | ||
// Implicit conversions do not kick in when importing | ||
implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState()) | ||
.foreach(addAccessibleMembers) | ||
} | ||
} | ||
|
||
/** | ||
* If `sym` exists, no symbol with the same name is already included, and it satisfies the | ||
* inclusion filter, then add it to the completions. | ||
*/ | ||
private def add(sym: Symbol)(implicit ctx: Context) = | ||
if (sym.exists && !completions.lookup(sym.name).exists && include(sym)) { | ||
completions.enter(sym) | ||
} | ||
|
||
/** Lookup members `name` from `site`, and try to add them to the completion list. */ | ||
private def addMember(site: Type, name: Name)(implicit ctx: Context) = | ||
if (!completions.lookup(name).exists) | ||
for (alt <- site.member(name).alternatives) add(alt.symbol) | ||
|
||
/** Include in completion sets only symbols that | ||
* 1. start with given name prefix, and | ||
* 2. do not contain '$' except in prefix where it is explicitly written by user, and | ||
* 3. are not a primary constructor, | ||
* 4. are the module class in case of packages, | ||
* 5. are mutable accessors, to exclude setters for `var`, | ||
* 6. have same term/type kind as name prefix given so far | ||
* | ||
* The reason for (2) is that we do not want to present compiler-synthesized identifiers | ||
* as completion results. However, if a user explicitly writes all '$' characters in an | ||
* identifier, we should complete the rest. | ||
*/ | ||
private def include(sym: Symbol)(implicit ctx: Context): Boolean = | ||
sym.name.startsWith(prefix) && | ||
!sym.name.toString.drop(prefix.length).contains('$') && | ||
!sym.isPrimaryConstructor && | ||
(!sym.is(Package) || !sym.moduleClass.exists) && | ||
!sym.is(allOf(Mutable, Accessor)) && | ||
( | ||
(mode.is(Mode.Term) && sym.isTerm) | ||
|| (mode.is(Mode.Type) && sym.isType) | ||
) | ||
|
||
/** | ||
* Find all the members of `site` that are accessible and which should be included in `info`. | ||
* | ||
* @param site The type to inspect. | ||
* @return The members of `site` that are accessible and pass the include filter of `info`. | ||
*/ | ||
private def accessibleMembers(site: Type)(implicit ctx: Context): Seq[Symbol] = site match { | ||
case site: NamedType if site.symbol.is(Package) => | ||
site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive. | ||
case _ => | ||
def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit = | ||
try buf ++= site.member(name).alternatives | ||
catch { case ex: TypeError => } | ||
site.memberDenots(takeAllFilter, appendMemberSyms).collect { | ||
case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess = true).symbol | ||
case _ => NoSymbol | ||
}.filter(_.exists) | ||
} | ||
|
||
/** Add all the accessible members of `site` in `info`. */ | ||
private def addAccessibleMembers(site: Type)(implicit ctx: Context): Unit = | ||
for (mbr <- accessibleMembers(site)) addMember(site, mbr.name) | ||
|
||
/** | ||
* Add in `info` the symbols that are imported by `ctx.importInfo`. If this is a wildcard import, | ||
* all the accessible members of the import's `site` are included. | ||
*/ | ||
private def addImportCompletions(implicit ctx: Context): Unit = { | ||
val imp = ctx.importInfo | ||
if (imp != null) { | ||
def addImport(name: TermName) = { | ||
addMember(imp.site, name) | ||
addMember(imp.site, name.toTypeName) | ||
} | ||
// FIXME: We need to also take renamed items into account for completions, | ||
Duhemm marked this conversation as resolved.
Show resolved
Hide resolved
|
||
// That means we have to return list of a pairs (Name, Symbol) instead of a list | ||
// of symbols from `completions`.!= | ||
for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported) | ||
if (imp.isWildcardImport) | ||
for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName)) | ||
addMember(imp.site, mbr.name) | ||
} | ||
} | ||
|
||
/** | ||
* Given `qual` of type T, finds all the types S such that there exists an implicit conversion | ||
* from T to S. | ||
* | ||
* @param qual The argument to which the implicit conversion should be applied. | ||
* @return The set of types that `qual` can be converted to. | ||
*/ | ||
private def implicitConversionTargets(qual: Tree)(implicit ctx: Context): Set[Type] = { | ||
val typer = ctx.typer | ||
val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.pos).allImplicits | ||
val targets = conversions.map(_.widen.finalResultType) | ||
interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %") | ||
targets | ||
} | ||
|
||
} | ||
|
||
/** | ||
* The completion mode: defines what kinds of symbols should be included in the completion | ||
* results. | ||
*/ | ||
private class Mode(val bits: Int) extends AnyVal { | ||
def is(other: Mode): Boolean = (bits & other.bits) == other.bits | ||
def |(other: Mode): Mode = new Mode(bits | other.bits) | ||
} | ||
private object Mode { | ||
/** No symbol should be included */ | ||
val None: Mode = new Mode(0) | ||
|
||
/** Term symbols are allowed */ | ||
val Term: Mode = new Mode(1) | ||
|
||
/** Type symbols are allowed */ | ||
val Type: Mode = new Mode(2) | ||
|
||
/** Both term and type symbols are allowed */ | ||
val Import: Mode = new Mode(4) | Term | Type | ||
} | ||
|
||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.