From 4e1a4d18b36a3d29b83a96a8a8315a2a0554a5c4 Mon Sep 17 00:00:00 2001 From: Jason Zaugg Date: Mon, 25 Jan 2021 17:00:45 +1000 Subject: [PATCH] Improvements to code assist in the REPL Re-enable acronmyn-style completion, e.g. getClass.gdm` offers `getDeclaredMethod[s]`. Disable the typo-matcher in JLine which tends to offer confusing Fix completion of keywored-starting-idents (e.g. `this.for` 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. REPL completion - Enable levenstien based typo matching ``` scala> scala.tools.nsc.util.EditDistance.levenshtien scala> scala.tools.nsc.util.EditDistance.levenshtein ``` --- .../scala/tools/nsc/interactive/Global.scala | 64 ++++----- .../scala/reflect/internal/Positions.scala | 3 +- .../scala/reflect/internal/Printers.scala | 40 +++--- .../tools/nsc/interpreter/jline/Reader.scala | 134 ++++++++++++++---- .../nsc/interpreter/shell/Completion.scala | 13 +- .../tools/nsc/interpreter/shell/ILoop.scala | 16 +-- .../nsc/interpreter/shell/LoopCommands.scala | 12 +- .../interpreter/shell/ReplCompletion.scala | 35 ++--- .../scala/tools/nsc/interpreter/IMain.scala | 11 +- .../tools/nsc/interpreter/Interface.scala | 11 +- .../interpreter/PresentationCompilation.scala | 133 ++++++++--------- .../nsc/interpreter/CompletionTest.scala | 46 +++--- 12 files changed, 296 insertions(+), 222 deletions(-) diff --git a/src/interactive/scala/tools/nsc/interactive/Global.scala b/src/interactive/scala/tools/nsc/interactive/Global.scala index c99fe6637aff..00743ffb8f7a 100644 --- a/src/interactive/scala/tools/nsc/interactive/Global.scala +++ b/src/interactive/scala/tools/nsc/interactive/Global.scala @@ -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" + // + // TODO: We could also complete the selection here to expand `ra"..."` 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 @@ -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) diff --git a/src/reflect/scala/reflect/internal/Positions.scala b/src/reflect/scala/reflect/internal/Positions.scala index 54183d7f3867..bfc995d96cc9 100644 --- a/src/reflect/scala/reflect/internal/Positions.scala +++ b/src/reflect/scala/reflect/internal/Positions.scala @@ -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 => diff --git a/src/reflect/scala/reflect/internal/Printers.scala b/src/reflect/scala/reflect/internal/Printers.scala index efc2da391027..8d62aea85931 100644 --- a/src/reflect/scala/reflect/internal/Printers.scala +++ b/src/reflect/scala/reflect/internal/Printers.scala @@ -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) diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala b/src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala index 7302966ac16d..d1cc9b209b3d 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/jline/Reader.scala @@ -14,15 +14,18 @@ package scala.tools.nsc.interpreter package jline import org.jline.builtins.InputRC +import org.jline.console.{CmdDesc, CmdLine} +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 @@ -122,17 +125,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) @@ -201,6 +273,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) } @@ -257,47 +335,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() + } } } } diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala index 17f8c72eb57e..389dd194e824 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/Completion.scala @@ -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))) } diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala index aece63c03b50..8f51bc84e691 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ILoop.scala @@ -228,7 +228,7 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, .map(d => CompletionResult(buffer, i, d.toDirectory.list.map(x => CompletionCandidate(x.name)).toList)) .getOrElse(NoCompletions) def listedIn(dir: Directory, name: String) = dir.list.filter(_.name.startsWith(name)).map(_.name).toList - def complete(buffer: String, cursor: Int): CompletionResult = + def complete(buffer: String, cursor: Int, filter: Boolean): CompletionResult = buffer.substring(0, cursor) match { case emptyWord(s) => listed(buffer, cursor, Directory.Current) case directorily(s) => listed(buffer, cursor, Option(Path(s))) @@ -247,13 +247,13 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, // complete settings name val settingsCompletion: Completion = new Completion { val trailingWord = """(\S+)$""".r.unanchored - def complete(buffer: String, cursor: Int): CompletionResult = { + def complete(buffer: String, cursor: Int, filter: Boolean): CompletionResult = { buffer.substring(0, cursor) match { case trailingWord(s) => - val maybes = intp.visibleSettings.filter(_.name.startsWith(s)).map(_.name) + val maybes = intp.visibleSettings.filter(x => if (filter) x.name.startsWith(s) else true).map(_.name) .filterNot(cond(_) { case "-"|"-X"|"-Y" => true }).sorted if (maybes.isEmpty) NoCompletions - else CompletionResult(buffer, cursor - s.length, maybes.map(CompletionCandidate(_))) + else CompletionResult(buffer, cursor - s.length, maybes.map(CompletionCandidate(_)), "", "") case _ => NoCompletions } } @@ -541,8 +541,8 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, MultiCompletion(shellCompletion, rc) } val shellCompletion = new Completion { - override def complete(buffer: String, cursor: Int) = - if (buffer.startsWith(":")) colonCompletion(buffer, cursor).complete(buffer, cursor) + override def complete(buffer: String, cursor: Int, filter: Boolean) = + if (buffer.startsWith(":")) colonCompletion(buffer, cursor).complete(buffer, cursor, filter) else NoCompletions } @@ -554,13 +554,13 @@ class ILoop(config: ShellConfig, inOverride: BufferedReader = null, // condition here is a bit weird because of the weird hack we have where // the first candidate having an empty defString means it's not really // completion, but showing the method signature instead - if (candidates.headOption.exists(_.defString.nonEmpty)) { + if (candidates.headOption.exists(_.name.nonEmpty)) { val prefix = if (completions == NoCompletions) "" else what.substring(0, completions.cursor) // hvesalai (emacs sbt-mode maintainer) says it's important to echo only once and not per-line echo( - candidates.map(c => s"[completions] $prefix${c.defString}") + candidates.map(c => s"[completions] $prefix${c.name}") .mkString("\n") ) } diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala index 1063971b5f2b..441829d86e15 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/LoopCommands.scala @@ -14,7 +14,6 @@ package scala.tools.nsc.interpreter package shell import java.io.{PrintWriter => JPrintWriter} - import scala.language.implicitConversions import scala.collection.mutable.ListBuffer import scala.tools.nsc.interpreter.ReplStrings.words @@ -60,6 +59,7 @@ trait LoopCommands { // subclasses may provide completions def completion: Completion = NoCompletion + override def toString(): String = name } object LoopCommand { def nullary(name: String, help: String, f: () => Result): LoopCommand = @@ -135,15 +135,15 @@ trait LoopCommands { case cmd :: Nil if !cursorAtName => cmd.completion case cmd :: Nil if cmd.name == name => NoCompletion case cmd :: Nil => - val completion = if (cmd.isInstanceOf[NullaryCmd] || cursor < line.length) cmd.name else cmd.name + " " + val completion = ":" + cmd.name new Completion { - def complete(buffer: String, cursor: Int) = - CompletionResult(buffer, cursor = 1, List(CompletionCandidate(completion))) + def complete(buffer: String, cursor: Int, filter: Boolean) = + CompletionResult(buffer, cursor = 1, List(CompletionCandidate(completion)), "", "") } case cmd :: rest => new Completion { - def complete(buffer: String, cursor: Int) = - CompletionResult(buffer, cursor = 1, cmds.map(cmd => CompletionCandidate(cmd.name))) + def complete(buffer: String, cursor: Int, filter: Boolean) = + CompletionResult(buffer, cursor = 1, cmds.map(cmd => CompletionCandidate(":" + cmd.name)), "", "") } } case _ => NoCompletion diff --git a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala index afbc38103e4d..8d0fb56fba02 100644 --- a/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala +++ b/src/repl-frontend/scala/tools/nsc/interpreter/shell/ReplCompletion.scala @@ -19,7 +19,7 @@ import scala.util.control.NonFatal */ class ReplCompletion(intp: Repl, val accumulator: Accumulator = new Accumulator) extends Completion { - def complete(buffer: String, cursor: Int): CompletionResult = { + def complete(buffer: String, cursor: Int, filter: Boolean): CompletionResult = { // special case for: // // scala> 1 @@ -30,13 +30,13 @@ class ReplCompletion(intp: Repl, val accumulator: Accumulator = new Accumulator) val bufferWithMultiLine = accumulator.toString + bufferWithVar val cursor1 = cursor + (bufferWithMultiLine.length - buffer.length) - codeCompletion(bufferWithMultiLine, cursor1) + codeCompletion(bufferWithMultiLine, cursor1, filter) } // A convenience for testing def complete(before: String, after: String = ""): CompletionResult = complete(before + after, before.length) - private def codeCompletion(buf: String, cursor: Int): CompletionResult = { + private def codeCompletion(buf: String, cursor: Int, filter: Boolean): CompletionResult = { require(cursor >= 0 && cursor <= buf.length) // secret handshakes @@ -49,37 +49,24 @@ class ReplCompletion(intp: Repl, val accumulator: Accumulator = new Accumulator) case Right(result) => try { buf match { case slashPrint() if cursor == buf.length => - CompletionResult(buf, cursor, CompletionCandidate.fromStrings("" :: Naming.unmangle(result.print) :: Nil)) + CompletionResult(buf, cursor, CompletionCandidate.fromStrings("" :: Naming.unmangle(result.print) :: Nil), "", "") case slashPrintRaw() if cursor == buf.length => - CompletionResult(buf, cursor, CompletionCandidate.fromStrings("" :: result.print :: Nil)) + CompletionResult(buf, cursor, CompletionCandidate.fromStrings("" :: result.print :: Nil), "", "") case slashTypeAt(start, end) if cursor == buf.length => - CompletionResult(buf, cursor, CompletionCandidate.fromStrings("" :: result.typeAt(start.toInt, end.toInt) :: Nil)) + CompletionResult(buf, cursor, CompletionCandidate.fromStrings("" :: result.typeAt(start.toInt, end.toInt) :: Nil), "", "") case _ => // under JLine 3, we no longer use the tabCount concept, so tabCount is always 1 // which always gives us all completions - val (c, r) = result.completionCandidates(tabCount = 1) - // 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. - if (r.nonEmpty && r.forall(!_.defString.startsWith("def"))) { - val groupByDef = r.groupBy(_.defString) - val allOverrideIsUniversal = groupByDef.filter(f => f._2.forall(_.isUniversal)).keySet - val allOverrideIsDeprecated = groupByDef.filter(f => f._2.forall(_.isDeprecated)).keySet - def isOverrideMethod(candidate: CompletionCandidate): Boolean = groupByDef(candidate.defString).size > 1 - val rewriteDecr = r.map(candidate => { - // If not all overloaded methods are deprecated, but they are overloaded methods, they (all) should be set to false. - val isUniv = if (!allOverrideIsUniversal.contains(candidate.defString) && isOverrideMethod(candidate)) false else candidate.isUniversal - val isDepr = if (!allOverrideIsDeprecated.contains(candidate.defString) && isOverrideMethod(candidate)) false else candidate.isDeprecated - candidate.copy(isUniversal = isUniv, isDeprecated = isDepr) - }) - CompletionResult(buf, c, rewriteDecr) - } else CompletionResult(buf, c, r) + val (c, r) = result.completionCandidates(filter, tabCount = 1) + val typeAtCursor = result.typeAt(cursor, cursor) + CompletionResult(buf, c, r, typeAtCursor, result.print) } } finally result.cleanup() } } catch { case NonFatal(e) => - // e.printStackTrace() +// if (intp.settings.debug) + e.printStackTrace() NoCompletions } } diff --git a/src/repl/scala/tools/nsc/interpreter/IMain.scala b/src/repl/scala/tools/nsc/interpreter/IMain.scala index 70ca0d8e227d..27a6af6b5a00 100644 --- a/src/repl/scala/tools/nsc/interpreter/IMain.scala +++ b/src/repl/scala/tools/nsc/interpreter/IMain.scala @@ -787,9 +787,12 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade // The source file contents only has the code originally input by the user, // with unit's body holding the synthetic trees. // When emitting errors, be careful not to refer to the synthetic code - private val unit = new CompilationUnit(new BatchSourceFile(if (synthetic) "" else label, line)) + // pad with a trailing " " so that the synthetic position for enclosing trees does not exactly coincide with the + // position of the user-written code, these seems to confuse the presentation compiler. + private val paddedLine = line + " " + private val unit = new CompilationUnit(new BatchSourceFile(if (synthetic) "" else label, paddedLine)) // a dummy position used for synthetic trees (needed for pres compiler to locate the trees for user input) - private val wholeUnit = Position.range(unit.source, 0, 0, line.length) + private val wholeUnit = Position.range(unit.source, 0, 0, paddedLine.length) private def storeInVal(tree: Tree): Tree = { val resName = newTermName(if (synthetic) freshInternalVarName() else freshUserVarName()) @@ -889,13 +892,13 @@ class IMain(val settings: Settings, parentClassLoaderOverride: Option[ClassLoade else ModuleDef(NoMods, readName, wrapperTempl)) if (isClassBased) - stats += q"""object $readName { val INSTANCE = new ${tq"""${readName.toTypeName}"""} }""" + stats += atPos(wholeUnit.focus)(q"""object $readName { val INSTANCE = new ${tq"""${readName.toTypeName}"""} }""") val unspliced = PackageDef(atPos(wholeUnit.focus)(Ident(lineRep.packageName)), stats.toList) unit.body = spliceUserCode.transform(unspliced) unit.encounteredXml(firstXmlPos) -// settings.Xprintpos.value = true + // settings.Xprintpos.value = true showCode(asCompactString(unit.body)) unit diff --git a/src/repl/scala/tools/nsc/interpreter/Interface.scala b/src/repl/scala/tools/nsc/interpreter/Interface.scala index 73f27ed749e9..790750daf367 100644 --- a/src/repl/scala/tools/nsc/interpreter/Interface.scala +++ b/src/repl/scala/tools/nsc/interpreter/Interface.scala @@ -323,21 +323,24 @@ trait PresentationCompilationResult { def candidates(tabCount: Int): (Int, List[String]) = completionCandidates(tabCount) match { case (cursor, cands) => - (cursor, cands.map(_.defString)) + (cursor, cands.map(_.name)) } - def completionCandidates(tabCount: Int = -1): (Int, List[CompletionCandidate]) + final def completionCandidates(tabCount: Int = -1): (Int, List[CompletionCandidate]) = completionCandidates(filter = true, tabCount) + def completionCandidates(filter: Boolean, tabCount: Int): (Int, List[CompletionCandidate]) } case class CompletionCandidate( - defString: String, + name: String, arity: CompletionCandidate.Arity = CompletionCandidate.Nullary, isDeprecated: Boolean = false, - isUniversal: Boolean = false) + isUniversal: Boolean = false, + declString: () => String = () => "") object CompletionCandidate { sealed trait Arity case object Nullary extends Arity case object Nilary extends Arity + case object Infix extends Arity case object Other extends Arity // purely for convenience def fromStrings(defStrings: List[String]): List[CompletionCandidate] = diff --git a/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala b/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala index 91df89362548..4adaaae8c4f2 100644 --- a/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala +++ b/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala @@ -12,7 +12,9 @@ package scala.tools.nsc.interpreter -import scala.reflect.internal.util.{Position, RangePosition, StringOps} +import scala.collection.mutable +import scala.reflect.internal.util.{Position, RangePosition} +import scala.tools.nsc.ast.parser.Tokens import scala.tools.nsc.backend.JavaPlatform import scala.tools.nsc.util.ClassPath import scala.tools.nsc.{Settings, interactive} @@ -22,7 +24,7 @@ import scala.tools.nsc.interpreter.Results.{Error, Result} trait PresentationCompilation { self: IMain => - private final val Cursor = IMain.DummyCursorFragment + " " + private final val Cursor = IMain.DummyCursorFragment /** Typecheck a line of REPL input, suitably wrapped with "interpreter wrapper" objects/classes, with the * presentation compiler. The result of this method gives access to the typechecked tree and to autocompletion @@ -34,7 +36,24 @@ trait PresentationCompilation { self: IMain => if (global == null) Left(Error) else { val pc = newPresentationCompiler() - val line1 = buf.patch(cursor, Cursor, 0) + def cursorIsInKeyword(): Boolean = { + val scanner = pc.newUnitParser(buf).newScanner() + scanner.init() + while (scanner.token != Tokens.EOF) { + val token = scanner.token + val o = scanner.offset + scanner.nextToken() + if ((o to scanner.lastOffset).contains(cursor)) { + return (!Tokens.isIdentifier(token) && pc.syntaxAnalyzer.token2name.contains(token)) + } + } + false + } + // Support completion of "def format = 42; for" by replacing the keyword with foo_CURSOR_ before + // typechecking. Only do this when needed to be able ot correctly return the type of `foo.bar` + // where `bar` is the complete name of a member. + val line1 = if (!cursorIsInKeyword()) buf else buf.patch(cursor, Cursor, 0) + val trees = pc.newUnitParser(line1).parseStats() val importer = global.mkImporter(pc) //println(s"pc: [[$line1]], <<${trees.size}>>") @@ -89,8 +108,6 @@ trait PresentationCompilation { self: IMain => interactiveGlobal } - private var lastCommonPrefixCompletion: Option[String] = None - abstract class PresentationCompileResult(val compiler: interactive.Global, val inputRange: Position, val cursor: Int, val buf: String) extends PresentationCompilationResult { val unit: compiler.RichCompilationUnit // depmet broken for constructors, can't be ctor arg @@ -120,15 +137,23 @@ trait PresentationCompilation { self: IMain => } } - def typeString(tree: compiler.Tree): String = - compiler.exitingTyper(tree.tpe.toString) + def typeString(tree: compiler.Tree): String = { + tree.tpe match { + case null | compiler.NoType | compiler.ErrorType => "" + case tp => compiler.exitingTyper(tp.toString) + } + } def treeString(tree: compiler.Tree): String = compiler.showCode(tree) override def print = { val tree = treeAt(inputRange) - treeString(tree) + " // : " + tree.tpe.safeToString + val tpString = typeString(tree) match { + case "" => "" + case s => " // : " + s + } + treeString(tree) + tpString } @@ -138,7 +163,7 @@ trait PresentationCompilation { self: IMain => val NoCandidates = (-1, Nil) type Candidates = (Int, List[CompletionCandidate]) - override def completionCandidates(tabCount: Int): Candidates = { + override def completionCandidates(filter: Boolean, tabCount: Int): Candidates = { import compiler._ import CompletionResult.NoResults @@ -161,76 +186,56 @@ 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): Candidates = { + def defStringCandidates(matching: List[Member], name: Name, isNew: Boolean) = { + val seen = new mutable.HashSet[Symbol]() val ccs = for { member <- matching - if member.symNameDropLocal == name + if seen.add(member.sym) sym <- if (member.sym.isClass && isNew) member.sym.info.decl(nme.CONSTRUCTOR).alternatives else member.sym.alternatives sugared = sym.sugaredSymbolOrSelf } yield { - val tp = member.prefix memberType sym - val desc = Seq(if (isMemberDeprecated(member)) "(deprecated)" else "", if (isMemberUniversal(member)) "(universal)" else "") - val methodOtherDesc = if (!desc.exists(_ != "")) "" else " " + desc.filter(_ != "").mkString(" ") CompletionCandidate( - defString = sugared.defStringSeenAs(tp) + methodOtherDesc, + name = member.symNameDropLocal.decoded, arity = memberArity(member), isDeprecated = isMemberDeprecated(member), - isUniversal = isMemberUniversal(member)) + isUniversal = isMemberUniversal(member), + declString = () => { + if (sym.isPackageObjectOrClass) "" + else { + val tp = member.prefix memberType sym + val desc = Seq(if (isMemberDeprecated(member)) "(deprecated)" else "", if (isMemberUniversal(member)) "(universal)" else "") + val methodOtherDesc = if (!desc.exists(_ != "")) "" else " " + desc.filter(_ != "").mkString(" ") + sugared.defStringSeenAs(tp) + methodOtherDesc + } + }) } - (cursor, CompletionCandidate("") :: ccs.distinct) + ccs } - def toCandidates(members: List[Member]): List[CompletionCandidate] = - members - .map(m => CompletionCandidate(m.symNameDropLocal.decoded, memberArity(m), isMemberDeprecated(m), isMemberUniversal(m))) - .sortBy(_.defString) val found = this.completionsAt(cursor) match { case NoResults => NoCandidates case r => def shouldHide(m: Member): Boolean = - tabCount == 0 && (isMemberDeprecated(m) || isMemberUniversal(m)) - val matching = r.matchingResults().filterNot(shouldHide) - val tabAfterCommonPrefixCompletion = lastCommonPrefixCompletion.contains(buf.substring(inputRange.start, cursor)) && matching.exists(_.symNameDropLocal == r.name) - val doubleTab = tabCount > 0 && matching.forall(_.symNameDropLocal == r.name) - if (tabAfterCommonPrefixCompletion || doubleTab) { - val pos1 = positionOf(cursor) - import compiler._ - val locator = new Locator(pos1) - val tree = locator locateIn unit.body - var isNew = false - new TreeStackTraverser { - override def traverse(t: Tree): Unit = { - if (t eq tree) { - isNew = path.dropWhile { case _: Select | _: Annotated => true; case _ => false}.headOption match { - case Some(_: New) => true - case _ => false - } - } else super.traverse(t) - } - }.traverse(unit.body) - defStringCandidates(matching, r.name, isNew) - } 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(shouldHide) - val memberCompletions: List[CompletionCandidate] = toCandidates(camelMatches) - def allowCompletion = ( - (memberCompletions.size == 1) - || CompletionResult.camelMatch(r.name)(r.name.newName(StringOps.longestCommonPrefix(memberCompletions.map(_.defString)))) - ) - if (memberCompletions.isEmpty) NoCandidates - else if (allowCompletion) (cursor - r.positionDelta, memberCompletions) - else (cursor, CompletionCandidate("") :: memberCompletions) - } else if (matching.nonEmpty && matching.forall(_.symNameDropLocal == r.name)) - NoCandidates // don't offer completion if the only option has been fully typed already - else { - // regular completion - (cursor - r.positionDelta, toCandidates(matching)) - } + filter && tabCount == 0 && (isMemberDeprecated(m) || isMemberUniversal(m)) + val matching = r.matchingResults(nameMatcher = if (filter) {entered => candidate => candidate.startsWith(entered)} else _ => _ => true).filterNot(shouldHide) + val pos1 = positionOf(cursor) + import compiler._ + val locator = new Locator(pos1) + val tree = locator locateIn unit.body + var isNew = false + new TreeStackTraverser { + override def traverse(t: Tree): Unit = { + if (t eq tree) { + isNew = path.dropWhile { case _: Select | _: Annotated => true; case _ => false}.headOption match { + case Some(_: New) => true + case _ => false + } + } else super.traverse(t) + } + }.traverse(unit.body) + val candidates = defStringCandidates(matching, r.name, isNew) + val pos = cursor - r.positionDelta + (pos, candidates.sortBy(_.name)) } - lastCommonPrefixCompletion = - if (found != NoCandidates && buf.length >= found._1) - Some(buf.substring(inputRange.start, found._1) + StringOps.longestCommonPrefix(found._2.map(_.defString))) - else - None found } diff --git a/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala b/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala index 870b9e987bb1..c32eed2182ca 100644 --- a/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala +++ b/test/junit/scala/tools/nsc/interpreter/CompletionTest.scala @@ -50,7 +50,7 @@ class CompletionTest { } val acc = new Accumulator val shellCompletion = new Completion { - override def complete(buffer: String, cursor: Int) = + override def complete(buffer: String, cursor: Int, filter: Boolean) = if (buffer.startsWith(":")) new CommandMock().colonCompletion(buffer, cursor).complete(buffer, cursor) else NoCompletions } @@ -106,7 +106,7 @@ class CompletionTest { checkExact(completer, "asInstanceO", "", includeUniversal = true)("asInstanceOf") // Output is sorted - 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.filter(!_.isUniversal).map(_.defString)) + 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.filter(!_.isUniversal).map(_.name)) // Enable implicits to check completion enrichment checkExact(completer, """'c'.toU""")("toUpper") @@ -172,11 +172,8 @@ class CompletionTest { def defStringConstructor(): Unit = { val intp = newIMain() val completer = new ReplCompletion(intp) - checkExact(completer, "class Shazam(i: Int); new Shaza")("Shazam") - checkExact(completer, "class Shazam(i: Int); new Shazam")(EmptyString, "def (i: Int): Shazam") - - checkExact(completer, "class Shazam(i: Int) { def this(x: String) = this(0) }; new Shaza")("Shazam") - checkExact(completer, "class Shazam(i: Int) { def this(x: String) = this(0) }; new Shazam")(EmptyString, "def (i: Int): Shazam", "def (x: String): Shazam") + checkExact(completer, "class Shazam(i: Int); new Shazam", result = _.declString())("def (i: Int): Shazam") + checkExact(completer, "class Shazam(i: Int) { def this(x: String) = this(0) }; new Shazam", result = _.declString())("def (i: Int): Shazam", "def (x: String): Shazam") } @Test @@ -212,7 +209,7 @@ class CompletionTest { | .map(_ + 1) /* then we do reverse */ | .rev""".stripMargin assertTrue( - completer.complete(withMultilineCommit).candidates.map(_.defString).contains("reverseMap") + completer.complete(withMultilineCommit).candidates.map(_.name).contains("reverseMap") ) val withInlineCommit = @@ -220,7 +217,7 @@ class CompletionTest { | .map(_ + 1) // then we do reverse | .rev""".stripMargin assertTrue( - completer.complete(withInlineCommit).candidates.map(_.defString).contains("reverseMap") + completer.complete(withInlineCommit).candidates.map(_.name).contains("reverseMap") ) } @@ -245,7 +242,9 @@ class CompletionTest { ) val candidates1 = completer.complete("Stale.ol").candidates assertEquals(2, candidates1.size) - assertEquals(candidates1.head.isDeprecated, false) + // Our JLine Reader is now responsible for only displaying @deprecated if all candidates with the name are + // deprecated. That isn't covered by this test. + assertEquals(candidates1.head.isDeprecated, true) assertEquals(candidates1.last.isDeprecated, false) } @@ -255,8 +254,8 @@ class CompletionTest { """object Stale { def oldie(i: Int) = ???; @deprecated("","") def oldie = ??? }""" ) val candidates1 = completer.complete("Stale.oldie").candidates - assertEquals(3, candidates1.size) - assertEquals(candidates1.filter(_.isDeprecated).map(_.defString.contains("deprecated")).head, true) + assertEquals(2, candidates1.size) + assertEquals(candidates1.filter(_.isDeprecated).map(_.declString().contains("deprecated")).head, true) assertEquals(candidates1.last.isDeprecated, false) } @@ -267,11 +266,11 @@ class CompletionTest { """object Stuff { @deprecated("","") def `this` = ??? ; @deprecated("","") def `that` = ??? }""" ) val candidates1 = completer.complete("Stale.oldie").candidates - assertEquals(2, candidates1.size) // When exactly matched, there is an empty character - assertTrue(candidates1.filter(_.defString.contains("oldie")).head.defString.contains("deprecated")) + assertEquals(1, candidates1.size) // When exactly matched, there is an empty character + assertTrue(candidates1.filter(_.declString().contains("oldie")).head.declString().contains("deprecated")) val candidates2 = completer.complete("Stuff.that").candidates - assertEquals(2, candidates2.size) - assertTrue(candidates2.filter(_.defString.contains("that")).head.defString.contains("deprecated")) + assertEquals(1, candidates2.size) + assertTrue(candidates2.filter(_.declString().contains("that")).head.declString().contains("deprecated")) } @Test @@ -301,9 +300,9 @@ class CompletionTest { """object A { class Type; object Term }""" ) val candidates1 = completer.complete("A.T").candidates - assertEquals("Term", candidates1.map(_.defString).mkString(" ")) + assertEquals("Term", candidates1.map(_.name).mkString(" ")) val candidates2 = completer.complete("import A.T").candidates - assertEquals("Term Type", candidates2.map(_.defString).sorted.mkString(" ")) + assertEquals("Term Type", candidates2.map(_.name).sorted.mkString(" ")) } @Test @@ -348,11 +347,12 @@ object Test2 { checkExact(completer, "test.Test.withoutParens.charA")("charAt") } - def checkExact(completer: Completion, before: String, after: String = "", includeUniversal: Boolean = false)(expected: String*): Unit = { - val actual = - completer.complete(before, after).candidates - .filter(c => includeUniversal || !c.isUniversal) - .map(_.defString) + def checkExact(completer: Completion, before: String, after: String = "", includeUniversal: Boolean = false, + result: CompletionCandidate => String = _.name)(expected: String*): Unit = { + val candidates = completer.complete(before, after).candidates + .filter(c => includeUniversal || !c.isUniversal) + val actual = candidates.map(result) assertEquals(expected.sorted.mkString(" "), actual.toSeq.distinct.sorted.mkString(" ")) } + }