Skip to content

Commit

Permalink
Improvements to code assist in the REPL
Browse files Browse the repository at this point in the history
Re-enable acronym-style completion, e.g. getClass.gdm`
offers `getDeclaredMethod[s]`.

Under JLine completion, move all filtering up in the UI layer.
Reimplement #9510 (dealing with overloads that contain some
deprecated alternatives) in the UI layer

Fix completion of keyword-starting-idents (e.g. `this.for<TAB>` offers
`formatted`.

Register a widget on CTRL-SHIFT-T that prints the type of the
expression at the cursor. A second invokation prints the
desugared AST.

Enable levenstien based typo matching, but disable it for short strings
which IMO tends to offer confusing results.

Enable levenstien based typo matching:

```
scala> scala.tools.nsc.util.EditDistance.levenshtien<TAB>

scala> scala.tools.nsc.util.EditDistance.levenshtein
```
  • Loading branch information
retronym committed Jun 5, 2021
1 parent 074cae1 commit f2c9727
Show file tree
Hide file tree
Showing 12 changed files with 295 additions and 222 deletions.
64 changes: 25 additions & 39 deletions src/interactive/scala/tools/nsc/interactive/Global.scala
Expand Up @@ -1197,54 +1197,36 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
override def positionDelta = 0
override def forImport: Boolean = false
}
private val CamelRegex = "([A-Z][^A-Z]*)".r
private def camelComponents(s: String, allowSnake: Boolean): List[String] = {
if (allowSnake && s.forall(c => c.isUpper || c == '_')) s.split('_').toList.filterNot(_.isEmpty)
else CamelRegex.findAllIn("X" + s).toList match { case head :: tail => head.drop(1) :: tail; case Nil => Nil }
}
def camelMatch(entered: Name): Name => Boolean = {
val enteredS = entered.toString
val enteredLowercaseSet = enteredS.toLowerCase().toSet
val allowSnake = !enteredS.contains('_')

{
candidate: Name =>
def candidateChunks = camelComponents(candidate.dropLocal.toString, allowSnake)
// Loosely based on IntelliJ's autocompletion: the user can just write everything in
// lowercase, as we'll let `isl` match `GenIndexedSeqLike` or `isLovely`.
def lenientMatch(entered: String, candidate: List[String], matchCount: Int): Boolean = {
candidate match {
case Nil => entered.isEmpty && matchCount > 0
case head :: tail =>
val enteredAlternatives = Set(entered, entered.capitalize)
val n = head.toIterable.lazyZip(entered).count {case (c, e) => c == e || (c.isUpper && c == e.toUpper)}
head.take(n).inits.exists(init =>
enteredAlternatives.exists(entered =>
lenientMatch(entered.stripPrefix(init), tail, matchCount + (if (init.isEmpty) 0 else 1))
)
)
}
}
val containsAllEnteredChars = {
// Trying to rule out some candidates quickly before the more expensive `lenientMatch`
val candidateLowercaseSet = candidate.toString.toLowerCase().toSet
enteredLowercaseSet.diff(candidateLowercaseSet).isEmpty
}
containsAllEnteredChars && lenientMatch(enteredS, candidateChunks, 0)
}
}
}

final def completionsAt(pos: Position): CompletionResult = {
val focus1: Tree = typedTreeAt(pos)
def typeCompletions(tree: Tree, qual: Tree, nameStart: Int, name: Name): CompletionResult = {
val qualPos = qual.pos
val allTypeMembers = typeMembers(qualPos).last
val saved = tree.tpe
// Force `typeMembers` to complete via the prefix, not the type of the Select itself.
tree.setType(ErrorType)
val allTypeMembers = try {
typeMembers(qualPos).last
} finally {
tree.setType(saved)
}
val positionDelta: Int = pos.start - nameStart
val subName: Name = name.newName(new String(pos.source.content, nameStart, pos.start - nameStart)).encodedName
CompletionResult.TypeMembers(positionDelta, qual, tree, allTypeMembers, subName)
}
focus1 match {
case Apply(Select(qual, name), _) if qual.hasAttachment[InterpolatedString.type] =>
// This special case makes CompletionTest.incompleteStringInterpolation work.
// In incomplete code, the parser treats `foo""` as a nested string interpolation, even
// though it is likely that the user wanted to complete `fooBar` before adding the closing brace.
// val fooBar = 42; s"abc ${foo<TAB>"
//
// TODO: We could also complete the selection here to expand `ra<TAB>"..."` to `raw"..."`.
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)
case imp@Import(i @ Ident(name), head :: Nil) if head.name == nme.ERROR =>
val allMembers = scopeMembers(pos)
val nameStart = i.pos.start
Expand All @@ -1259,9 +1241,13 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
}
case sel@Select(qual, name) =>
val qualPos = qual.pos
def fallback = qualPos.end + 2
val effectiveQualEnd = if (qualPos.isRange) qualPos.end else qualPos.point - 1
def fallback = {
effectiveQualEnd + 2
}
val source = pos.source
val nameStart: Int = (focus1.pos.end - 1 to qualPos.end by -1).find(p =>

val nameStart: Int = (focus1.pos.end - 1 to effectiveQualEnd by -1).find(p =>
source.identifier(source.position(p)).exists(_.length == 0)
).map(_ + 1).getOrElse(fallback)
typeCompletions(sel, qual, nameStart, name)
Expand Down
3 changes: 2 additions & 1 deletion src/reflect/scala/reflect/internal/Positions.scala
Expand Up @@ -345,7 +345,8 @@ trait Positions extends api.Positions { self: SymbolTable =>
if (t.pos includes pos) {
if (isEligible(t)) last = t
super.traverse(t)
} else t match {
}
t match {
case mdef: MemberDef =>
val annTrees = mdef.mods.annotations match {
case Nil if mdef.symbol != null =>
Expand Down
40 changes: 22 additions & 18 deletions src/reflect/scala/reflect/internal/Printers.scala
Expand Up @@ -781,26 +781,30 @@ trait Printers extends api.Printers { self: SymbolTable =>
print("class ", printedName(name))
printTypeParams(tparams)

val build.SyntacticClassDef(_, _, _, ctorMods, vparamss, earlyDefs, parents, selfType, body) = cl: @unchecked

// constructor's modifier
if (ctorMods.hasFlag(AccessFlags) || ctorMods.hasAccessBoundary) {
print(" ")
printModifiers(ctorMods, primaryCtorParam = false)
}
cl match {
case build.SyntacticClassDef(_, _, _, ctorMods, vparamss, earlyDefs, parents, selfType, body) =>
// constructor's modifier
if (ctorMods.hasFlag(AccessFlags) || ctorMods.hasAccessBoundary) {
print(" ")
printModifiers(ctorMods, primaryCtorParam = false)
}

def printConstrParams(ts: List[ValDef]): Unit = {
parenthesize() {
printImplicitInParamsList(ts)
printSeq(ts)(printVParam(_, primaryCtorParam = true))(print(", "))
}
}
// constructor's params processing (don't print single empty constructor param list)
vparamss match {
case Nil | List(Nil) if !mods.isCase && !ctorMods.hasFlag(AccessFlags) =>
case _ => vparamss foreach printConstrParams
def printConstrParams(ts: List[ValDef]): Unit = {
parenthesize() {
printImplicitInParamsList(ts)
printSeq(ts)(printVParam(_, primaryCtorParam = true))(print(", "))
}
}
// constructor's params processing (don't print single empty constructor param list)
vparamss match {
case Nil | List(Nil) if !mods.isCase && !ctorMods.hasFlag(AccessFlags) =>
case _ => vparamss foreach printConstrParams
}
parents
case _ =>
// Can get here with erroneous code, like `{@deprecatedName `
Nil
}
parents
}

// get trees without default classes and traits (when they are last)
Expand Down
133 changes: 108 additions & 25 deletions src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala
Expand Up @@ -14,15 +14,17 @@ package scala.tools.nsc.interpreter
package jline

import org.jline.builtins.InputRC
import org.jline.keymap.KeyMap
import org.jline.reader.Parser.ParseContext
import org.jline.reader._
import org.jline.reader.impl.{DefaultParser, LineReaderImpl}
import org.jline.reader.impl.{CompletionMatcherImpl, DefaultParser, LineReaderImpl}
import org.jline.terminal.Terminal

import java.io.{ByteArrayInputStream, File}
import java.net.{MalformedURLException, URL}
import java.util.{List => JList}
import scala.io.Source
import scala.reflect.internal.Chars
import scala.tools.nsc.interpreter.shell.{Accumulator, ShellConfig}
import scala.util.Using
import scala.util.control.NonFatal
Expand Down Expand Up @@ -122,17 +124,86 @@ object Reader {
.variable(SECONDARY_PROMPT_PATTERN, config.encolor(config.continueText)) // Continue prompt
.variable(WORDCHARS, LineReaderImpl.DEFAULT_WORDCHARS.filterNot("*?.[]~=/&;!#%^(){}<>".toSet))
.option(Option.DISABLE_EVENT_EXPANSION, true) // Otherwise `scala> println(raw"\n".toList)` gives `List(n)` !!
.option(Option.COMPLETE_MATCHER_CAMELCASE, true)
}
object customCompletionMatcher extends CompletionMatcherImpl {
override def compile(options: java.util.Map[LineReader.Option, java.lang.Boolean], prefix: Boolean, line: CompletingParsedLine, caseInsensitive: Boolean, errors: Int, originalGroupName: String): Unit = {
val errorsReduced = line.wordCursor() match {
case 0 | 1 | 2 | 3 => 0 // disable JLine's levenshtein-distance based typo matcher for short strings
case 4 | 5 => math.max(errors, 1)
case _ => errors
}
super.compile(options, prefix, line, caseInsensitive, errorsReduced, originalGroupName)

// TODO JLINE All of this can/must be removed after the next JLine upgrade
matchers.remove(matchers.size() - 2) // remove ty
val wd = line.word();
val wdi = if (caseInsensitive) wd.toLowerCase() else wd
val typoMatcherWord = if (prefix) wdi.substring(0, line.wordCursor()) else wdi
val fixedTypoMatcher = typoMatcher(
typoMatcherWord,
errorsReduced,
!caseInsensitive, // Fixed in JLine https://github.com/jline/jline3/pull/647, remove the negation when upgrading!
originalGroupName
)
matchers.add(matchers.size - 2, fixedTypoMatcher)
}

override def matches(candidates: JList[Candidate]): JList[Candidate] = {
val matching = super.matches(candidates)
matching
}
}

builder.completionMatcher(customCompletionMatcher)

val reader = builder.build()
try inputrcFileContents.foreach(f => InputRC.configure(reader, new ByteArrayInputStream(f))) catch {
case NonFatal(_) =>
} //ignore

val keyMap = reader.getKeyMaps.get("main")

object ScalaShowType {
val Name = "scala-show-type"
private var lastInvokeLocation: Option[(String, Int)] = None
def apply(): Boolean = {
val nextInvokeLocation = Some((reader.getBuffer.toString, reader.getBuffer.cursor()))
val cursor = reader.getBuffer.cursor()
val text = reader.getBuffer.toString
val result = completer.complete(text, cursor, filter = true)
if (lastInvokeLocation == nextInvokeLocation) {
showTree(result)
lastInvokeLocation = None
} else {
showType(result)
lastInvokeLocation = nextInvokeLocation
}
true
}
def showType(result: shell.CompletionResult): Unit = {
reader.getTerminal.writer.println()
reader.getTerminal.writer.println(result.typeAtCursor)
reader.callWidget(LineReader.REDRAW_LINE)
reader.callWidget(LineReader.REDISPLAY)
reader.getTerminal.flush()
}
def showTree(result: shell.CompletionResult): Unit = {
reader.getTerminal.writer.println()
reader.getTerminal.writer.println(Naming.unmangle(result.typedTree))
reader.callWidget(LineReader.REDRAW_LINE)
reader.callWidget(LineReader.REDISPLAY)
reader.getTerminal.flush()
}
}
reader.getWidgets().put(ScalaShowType.Name, () => ScalaShowType())

locally {
import LineReader._
// VIINS, VICMD, EMACS
val keymap = if (config.viMode) VIINS else EMACS
reader.getKeyMaps.put(MAIN, reader.getKeyMaps.get(keymap));
keyMap.bind(new Reference(ScalaShowType.Name), KeyMap.ctrl('T'))
}
def secure(p: java.nio.file.Path): Unit = {
try scala.reflect.internal.util.OwnerOnlyChmod.chmodFileOrCreateEmpty(p)
Expand Down Expand Up @@ -201,6 +272,12 @@ object Reader {
val (wordCursor, wordIndex) = current match {
case Some(t) if t.isIdentifier =>
(cursor - t.start, tokens.indexOf(t))
case Some(t) =>
val isIdentifierStartKeyword = (t.start until t.end).forall(i => Chars.isIdentifierPart(line.charAt(i)))
if (isIdentifierStartKeyword)
(cursor - t.start, tokens.indexOf(t))
else
(0, -1)
case _ =>
(0, -1)
}
Expand Down Expand Up @@ -257,47 +334,53 @@ object Reader {
* It delegates both interfaces to an underlying `Completion`.
*/
class Completion(delegate: shell.Completion) extends shell.Completion with Completer {
var lastPrefix: String = ""
require(delegate != null)
// REPL Completion
def complete(buffer: String, cursor: Int): shell.CompletionResult = delegate.complete(buffer, cursor)
def complete(buffer: String, cursor: Int, filter: Boolean): shell.CompletionResult = delegate.complete(buffer, cursor, filter)

// JLine Completer
def complete(lineReader: LineReader, parsedLine: ParsedLine, newCandidates: JList[Candidate]): Unit = {
def candidateForResult(line: String, cc: CompletionCandidate): Candidate = {
val value = if (line.startsWith(":")) ":" + cc.defString else cc.defString
val displayed = cc.defString + (cc.arity match {
def candidateForResult(cc: CompletionCandidate, deprecated: Boolean, universal: Boolean): Candidate = {
val value = cc.name
val displayed = cc.name + (cc.arity match {
case CompletionCandidate.Nullary => ""
case CompletionCandidate.Nilary => "()"
case _ => "("
})
val group = null // results may be grouped
val descr = // displayed alongside
if (cc.isDeprecated) "deprecated"
else if (cc.isUniversal) "universal"
if (deprecated) "deprecated"
else if (universal) "universal"
else null
val suffix = null // such as slash after directory name
val key = null // same key implies mergeable result
val complete = false // more to complete?
new Candidate(value, displayed, group, descr, suffix, key, complete)
}
val result = complete(parsedLine.line, parsedLine.cursor)
result.candidates.map(_.defString) match {
// the presence of the empty string here is a signal that the symbol
// is already complete and so instead of completing, we want to show
// the user the method signature. there are various JLine 3 features
// one might use to do this instead; sticking to basics for now
case "" :: defStrings if defStrings.nonEmpty =>
// specifics here are cargo-culted from Ammonite
lineReader.getTerminal.writer.println()
for (cc <- result.candidates.tail)
lineReader.getTerminal.writer.println(cc.defString)
lineReader.callWidget(LineReader.REDRAW_LINE)
lineReader.callWidget(LineReader.REDISPLAY)
lineReader.getTerminal.flush()
// normal completion
case _ =>
for (cc <- result.candidates)
newCandidates.add(candidateForResult(result.line, cc))
val result = complete(parsedLine.line, parsedLine.cursor, filter = false)
for (group <- result.candidates.groupBy(_.name)) {
// scala/bug#12238
// Currently, only when all methods are Deprecated should they be displayed `Deprecated` to users. Only handle result of PresentationCompilation#toCandidates.
// We don't handle result of PresentationCompilation#defStringCandidates, because we need to show the deprecated here.
val allDeprecated = group._2.forall(_.isDeprecated)
val allUniversal = group._2.forall(_.isUniversal)
group._2.foreach(cc => newCandidates.add(candidateForResult(cc, allDeprecated, allUniversal)))
}

val parsedLineWord = parsedLine.word()
result.candidates.filter(_.name == parsedLineWord) match {
case Nil =>
case exacts =>
val declStrings = exacts.map(_.declString()).filterNot(_ == "")
if (declStrings.nonEmpty) {
lineReader.getTerminal.writer.println()
for (declString <- declStrings)
lineReader.getTerminal.writer.println(declString)
lineReader.callWidget(LineReader.REDRAW_LINE)
lineReader.callWidget(LineReader.REDISPLAY)
lineReader.getTerminal.flush()
}
}
}
}
Expand Down
Expand Up @@ -14,22 +14,23 @@ package scala.tools.nsc.interpreter
package shell

trait Completion {
def complete(buffer: String, cursor: Int): CompletionResult
final def complete(buffer: String, cursor: Int): CompletionResult = complete(buffer, cursor, filter = true)
def complete(buffer: String, cursor: Int, filter: Boolean): CompletionResult
}
object NoCompletion extends Completion {
def complete(buffer: String, cursor: Int) = NoCompletions
def complete(buffer: String, cursor: Int, filter: Boolean) = NoCompletions
}

case class CompletionResult(line: String, cursor: Int, candidates: List[CompletionCandidate]) {
case class CompletionResult(line: String, cursor: Int, candidates: List[CompletionCandidate], typeAtCursor: String = "", typedTree: String = "") {
final def orElse(other: => CompletionResult): CompletionResult =
if (candidates.nonEmpty) this else other
}
object CompletionResult {
val empty: CompletionResult = NoCompletions
}
object NoCompletions extends CompletionResult("", -1, Nil)
object NoCompletions extends CompletionResult("", -1, Nil, "", "")

case class MultiCompletion(underlying: Completion*) extends Completion {
override def complete(buffer: String, cursor: Int) =
underlying.foldLeft(CompletionResult.empty)((r, c) => r.orElse(c.complete(buffer, cursor)))
override def complete(buffer: String, cursor: Int, filter: Boolean) =
underlying.foldLeft(CompletionResult.empty)((r,c) => r.orElse(c.complete(buffer, cursor, filter)))
}

0 comments on commit f2c9727

Please sign in to comment.