Skip to content
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

Tab completion for aliased constructors in REPL #9754

Merged
merged 1 commit into from
Jan 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,7 @@ trait CompilerControl { self: Global =>
val sym: Symbol
val tpe: Type
val accessible: Boolean
val aliasInfo: Option[ScopeMember]
def implicitlyAdded = false
def symNameDropLocal: Name = if (sym.name.isTermName) sym.name.dropLocal else sym.name

Expand All @@ -298,7 +299,8 @@ trait CompilerControl { self: Global =>
tpe: Type,
accessible: Boolean,
inherited: Boolean,
viaView: Symbol) extends Member {
viaView: Symbol,
aliasInfo: Option[ScopeMember] = None) extends Member {
// should be a case class parameter, but added as a var instead to preserve compatibility with the IDE
var prefix: Type = NoType
override def implicitlyAdded = viaView != NoSymbol
Expand All @@ -308,7 +310,8 @@ trait CompilerControl { self: Global =>
sym: Symbol,
tpe: Type,
accessible: Boolean,
viaImport: Tree) extends Member {
viaImport: Tree,
aliasInfo: Option[ScopeMember] = None) extends Member {
// should be a case class parameter, but added as a var instead to preserve compatibility with the IDE
var prefix: Type = NoType
}
Expand Down
44 changes: 32 additions & 12 deletions src/interactive/scala/tools/nsc/interactive/Global.scala
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")

private[interactive] def getScopeCompletion(pos: Position, response: Response[List[Member]]): Unit = {
informIDE("getScopeCompletion" + pos)
respond(response) { scopeMembers(pos) }
respond(response) { scopeMemberFlatten(scopeMembers(pos)) }
}

private class Members[M <: Member] extends LinkedHashMap[Name, Set[M]] {
Expand Down Expand Up @@ -1043,9 +1043,15 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
locals.add(sym, pre, implicitlyAdded = false) { (s, st) =>
// imported val and var are always marked as inaccessible, but they could be accessed through their getters. scala/bug#7995
val member = if (s.hasGetter)
new ScopeMember(s, st, context.isAccessible(s.getter, pre, superAccess = false), viaImport)
else
new ScopeMember(s, st, context.isAccessible(s, pre, superAccess = false), viaImport)
ScopeMember(s, st, context.isAccessible(s.getter, pre, superAccess = false), viaImport)
else {
if (s.isAliasType) {
val aliasInfo = ScopeMember(s, st, context.isAccessible(s, pre, superAccess = false), viaImport)
ScopeMember(s.info.typeSymbol, s.info.typeSymbol.tpe,
context.isAccessible(s.info.typeSymbol, pre, superAccess = false), viaImport,
aliasInfo = Some(aliasInfo))
} else ScopeMember(s, st, context.isAccessible(s, pre, superAccess = false), viaImport)
}
member.prefix = pre
member
}
Expand Down Expand Up @@ -1181,14 +1187,23 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
def matchingResults(nameMatcher: (Name) => Name => Boolean = entered => candidate => candidate.startsWith(entered)): List[M] = {
val enteredName = if (name == nme.ERROR) nme.EMPTY else name
val matcher = nameMatcher(enteredName)
results filter { (member: Member) =>
results.filter { (member: Member) =>
val symbol = member.sym
def isStable = member.tpe.isStable || member.sym.isStable || member.sym.getterIn(member.sym.owner).isStable
def isJunk = !symbol.exists || symbol.name.isEmpty || !isIdentifierStart(member.sym.name.charAt(0)) // e.g. <byname>
def nameTypeOk = forImport || // Completing an import: keep terms and types.
symbol.name.isTermName == name.isTermName || // Keep names of the same type
name.isTypeName && isStable // Completing a type: keep stable terms (paths)
!isJunk && member.accessible && !symbol.isConstructor && (name.isEmpty || matcher(member.sym.name) && nameTypeOk)
def nameTypeOk: Boolean = {
forImport || // Completing an import: keep terms and types.
symbol.name.isTermName == name.isTermName || // Keep names of the same type
name.isTypeName && isStable // Completing a type: keep stable terms (paths)
}
// scala/bug#11846 aliasInfo should be match
def aliasTypeOk: Boolean = {
matcher(member.aliasInfo.map(_.sym.name).getOrElse(NoSymbol.name)) && !forImport && symbol.name.isTermName == name.isTermName
}

!isJunk && member.accessible && !symbol.isConstructor && (name.isEmpty || (matcher(member.sym.name) || aliasTypeOk)
&& nameTypeOk)

}
}
}
Expand All @@ -1208,6 +1223,11 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
}
}

private def scopeMemberFlatten(members: List[ScopeMember]): List[ScopeMember] = {
val (infoWithoutAlias, infoWithAlias) = members.partition(_.aliasInfo.isEmpty)
infoWithoutAlias ++ infoWithAlias ++ infoWithAlias.flatten(_.aliasInfo)
}

final def completionsAt(pos: Position): CompletionResult = {
val focus1: Tree = typedTreeAt(pos)
def typeCompletions(tree: Tree, qual: Tree, nameStart: Int, name: Name): CompletionResult = {
Expand Down Expand Up @@ -1235,13 +1255,13 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
val allMembers = scopeMembers(pos)
val positionDelta: Int = pos.start - focus1.pos.start
val subName = name.subName(0, positionDelta)
CompletionResult.ScopeMembers(positionDelta, allMembers, subName, forImport = false)
CompletionResult.ScopeMembers(positionDelta, scopeMemberFlatten(allMembers), subName, forImport = false)
case imp@Import(i @ Ident(name), head :: Nil) if head.name == nme.ERROR =>
val allMembers = scopeMembers(pos)
val nameStart = i.pos.start
val positionDelta: Int = pos.start - nameStart
val subName = name.subName(0, pos.start - i.pos.start)
CompletionResult.ScopeMembers(positionDelta, allMembers, subName, forImport = true)
CompletionResult.ScopeMembers(positionDelta, scopeMemberFlatten(allMembers), subName, forImport = true)
case imp@Import(qual, selectors) =>
selectors.reverseIterator.find(_.namePos <= pos.start) match {
case None => CompletionResult.NoResults
Expand All @@ -1264,7 +1284,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
val allMembers = scopeMembers(pos)
val positionDelta: Int = pos.start - focus1.pos.start
val subName = name.subName(0, positionDelta)
CompletionResult.ScopeMembers(positionDelta, allMembers, subName, forImport = false)
CompletionResult.ScopeMembers(positionDelta, scopeMemberFlatten(allMembers), subName, forImport = false)
case _ =>
CompletionResult.NoResults
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ class Completion(delegate: shell.Completion) extends shell.Completion with Compl
}

val parsedLineWord = parsedLine.word()
result.candidates.filter(_.name == parsedLineWord) match {
result.candidates.filter(c => c.name == parsedLineWord || c.alias.fold(false)(a => a == parsedLineWord)) match {
case Nil =>
case exacts =>
val declStrings = exacts.map(_.declString()).filterNot(_ == "")
Expand Down
4 changes: 3 additions & 1 deletion src/repl/scala/tools/nsc/interpreter/Interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,9 @@ case class CompletionCandidate(
arity: CompletionCandidate.Arity = CompletionCandidate.Nullary,
isDeprecated: Boolean = false,
isUniversal: Boolean = false,
declString: () => String = () => "")
declString: () => String = () => "",
alias: Option[String] = None
)
object CompletionCandidate {
sealed trait Arity
case object Nullary extends Arity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ trait PresentationCompilation { self: IMain =>
if (m.sym.paramss.isEmpty) CompletionCandidate.Nullary
else if (m.sym.paramss.size == 1 && m.sym.paramss.head.isEmpty) CompletionCandidate.Nilary
else CompletionCandidate.Other
def defStringCandidates(matching: List[Member], name: Name, isNew: Boolean) = {
def defStringCandidates(matching: List[Member], isNew: Boolean): List[CompletionCandidate] = {
val seen = new mutable.HashSet[Symbol]()
val ccs = for {
member <- matching
Expand All @@ -232,7 +232,9 @@ trait PresentationCompilation { self: IMain =>
val methodOtherDesc = if (!desc.exists(_ != "")) "" else " " + desc.filter(_ != "").mkString(" ")
sugared.defStringSeenAs(tp) + methodOtherDesc
}
})
},
alias = member.aliasInfo.fold[Option[String]](None)(s => Some(s.sym.nameString))
)
}
ccs
}
Expand All @@ -257,7 +259,7 @@ trait PresentationCompilation { self: IMain =>
} else super.traverse(t)
}
}.traverse(unit.body)
val candidates = defStringCandidates(matching, r.name, isNew)
val candidates = defStringCandidates(matching, isNew)
val pos = cursor - r.positionDelta
(pos, candidates.sortBy(_.name))
}
Expand Down
20 changes: 20 additions & 0 deletions test/junit/scala/tools/nsc/interpreter/CompletionTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -356,4 +356,24 @@ object Test2 {
assertEquals(expected.sorted.mkString(" "), actual.toSeq.distinct.sorted.mkString(" "))
}

@Test
def ignoreAlias(): Unit = {
val (completer, _, _) = interpretLines(
"""class Foo(i: Int) { def this(s: String) = this(s.toInt) }""",
"""type Bar = Foo"""
)
// We not only keep the original `type Bar = Bar`, but also add more detailed candidates
val candidates = completer.complete("new Bar").candidates
//type Bar = Bar
//def <init>(i: Int): Foo
//def <init>(s: String): Foo
assertEquals(3, candidates.size)
assertEquals("type Bar = Bar", candidates.head.declString.apply())
assertEquals("def <init>(i: Int): Foo", candidates(1).declString.apply())
assertEquals("def <init>(s: String): Foo", candidates(2).declString.apply())

val candidates1 = completer.complete("new Foo").candidates
assertEquals(2, candidates1.size)
}

}