From 0a8c4b263e979a7ed9fc9172ebc5f51e8093f3d5 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Tue, 8 Sep 2015 23:32:56 +1000 Subject: Camel Case and JavaBean completion This is just too useful to leave on the cutting room floor. ``` scala> classOf[String].enclo scala> classOf[String].getEnclosing getEnclosingClass getEnclosingConstructor getEnclosingMethod scala> classOf[String].simpl scala> classOf[String].getSimpleName type X = global.TTWD scala> type X = global.TypeTreeWithDeferredRefCheck ``` I revised the API of `matchingResults` as it was clunky to reuse the filtering on accessibility and term/type-ness while providing a custom name matcher. --- .../scala/tools/nsc/interactive/Global.scala | 34 +++++++++++++++++----- .../PresentationCompilerCompleter.scala | 23 ++++++++++----- .../tools/nsc/interpreter/CompletionTest.scala | 11 +++++++ 3 files changed, 53 insertions(+), 15 deletions(-) diff --git a/src/interactive/scala/tools/nsc/interactive/Global.scala b/src/interactive/scala/tools/nsc/interactive/Global.scala index b9d06b3633..703da0b947 100644 --- a/src/interactive/scala/tools/nsc/interactive/Global.scala +++ b/src/interactive/scala/tools/nsc/interactive/Global.scala @@ -1159,8 +1159,14 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") /** The (possibly partial) detected that precedes the cursor */ def name: Name def positionDelta: Int - def matchingResults(matcher: (M, Name) => Boolean = CompletionResult.prefixMatcher): List[M] = { - results filter (r => matcher(r, name)) + 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) => + val symbol = member.sym + def isStable = member.tpe.isStable || member.sym.isStable || member.sym.getterIn(member.sym.owner).isStable + member.accessible && !symbol.isConstructor && (name.isEmpty || matcher(member.sym.name) && (symbol.name.isTermName == name.isTermName || name.isTypeName && isStable)) + } } } object CompletionResult { @@ -1176,12 +1182,24 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "") override def positionDelta = 0 } - val prefixMatcher = (member: Member, name: Name) => { - val symbol = member.sym - val prefix = if (name == nme.ERROR) nme.EMPTY else name - def isStable = member.tpe.isStable || member.sym.isStable || member.sym.getterIn(member.sym.owner).isStable - val nameMatches = !symbol.isConstructor && (prefix.isEmpty || symbol.name.startsWith(prefix) && (symbol.name.isTermName == prefix.isTermName || prefix.isTypeName && isStable)) - nameMatches && member.accessible + private val CamelRegex = "([A-Z][^A-Z]*)".r + private def camelComponents(s: String): List[String] = { + CamelRegex.findAllIn("X" + s).toList match { case head :: tail => head.drop(1) :: tail; case Nil => Nil } + } + def camelMatch(entered: Name): Name => Boolean = { + val chunks: List[String] = camelComponents(entered.toString) + + (candidate: Name) => { + val candidateChunks = camelComponents(candidate.toString) + val exactCamelMatch = + (chunks corresponds candidateChunks.take(chunks.length))((x, y) => y.startsWith(x)) + def beanCamelMatch = candidateChunks match { + case ("get" | "is") :: tail => + (chunks corresponds tail.take(chunks.length))((x, y) => y.toLowerCase.startsWith(x.toLowerCase)) + case _ => false + } + exactCamelMatch || beanCamelMatch + } } } diff --git a/src/repl/scala/tools/nsc/interpreter/PresentationCompilerCompleter.scala b/src/repl/scala/tools/nsc/interpreter/PresentationCompilerCompleter.scala index f6b6ff440b..7cff24cfa3 100644 --- a/src/repl/scala/tools/nsc/interpreter/PresentationCompilerCompleter.scala +++ b/src/repl/scala/tools/nsc/interpreter/PresentationCompilerCompleter.scala @@ -77,14 +77,23 @@ class PresentationCompilerCompleter(intp: IMain) extends Completion with ScalaCo val tabAfterCommonPrefixCompletion = lastCommonPrefixCompletion.contains(buf.substring(0, cursor)) && matching.exists(_.symNameDropLocal == r.name) val doubleTab = tabCount > 0 && matching.forall(_.symNameDropLocal == r.name) if (tabAfterCommonPrefixCompletion || doubleTab) defStringCandidates(matching, r.name) + else if (matching.isEmpty) { + // Lenient matching based on camel case and on eliding JavaBean "get" / "is" boilerplate + val camelMatches: List[Member] = r.matchingResults(CompletionResult.camelMatch(_)).filterNot(isInterpreterWrapperMember) + val memberCompletions = camelMatches.map(_.symNameDropLocal.decoded).distinct.sorted + def allowCompletion = ( + (memberCompletions.size == 1) + || CompletionResult.camelMatch(r.name)(r.name.newName(StringOps.longestCommonPrefix(memberCompletions))) + ) + if (memberCompletions.isEmpty) Completion.NoCandidates + else if (allowCompletion) Candidates(cursor + r.positionDelta, memberCompletions) + else Candidates(cursor, "" :: memberCompletions) + } else if (matching.nonEmpty && matching.forall(_.symNameDropLocal == r.name)) + Completion.NoCandidates // don't offer completion if the only option has been fully typed already else { - if (matching.nonEmpty && matching.forall(_.symNameDropLocal == r.name)) - Completion.NoCandidates // don't offer completion if the only option has been fully typed already - else { - // regular completion - val memberCompletions: List[String] = matching.map(_.symNameDropLocal.decoded).distinct.sorted - Candidates(cursor + r.positionDelta, memberCompletions) - } + // regular completion + val memberCompletions: List[String] = matching.map(_.symNameDropLocal.decoded).distinct.sorted + Candidates(cursor + r.positionDelta, memberCompletions) } } lastCommonPrefixCompletion = diff --git a/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala b/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala index 75ab4c605f..a55a3c66c0 100644 --- a/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala +++ b/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala @@ -51,6 +51,17 @@ class CompletionTest { assertEquals(List("prefix_aaa", "prefix_nnn", "prefix_zzz"), completer.complete("""class C { def prefix_nnn = 0; def prefix_zzz = 0; def prefix_aaa = 0; prefix_""").candidates) } + @Test + def camelCompletions(): Unit = { + val intp = newIMain() + val completer = new PresentationCompilerCompleter(intp) + checkExact(completer, "object O { def theCatSatOnTheMat = 1 }; import O._; tCSO")("theCatSatOnTheMat") + checkExact(completer, "object O { def getBlerganator = 1 }; import O._; blerga")("getBlerganator") + checkExact(completer, "object O { def xxxxYyyyyZzzz = 1; def xxxxYyZeee = 1 }; import O._; xYZ")("", "xxxxYyyyyZzzz", "xxxxYyZeee") + checkExact(completer, "object O { def xxxxYyyyyZzzz = 1; def xxxxYyyyyZeee = 1 }; import O._; xYZ")("xxxxYyyyyZzzz", "xxxxYyyyyZeee") + checkExact(completer, "object O { class AbstractMetaFactoryFactory }; new O.AMFF")("AbstractMetaFactoryFactory") + } + @Test def previousLineCompletions(): Unit = { val intp = newIMain() -- cgit v1.2.3