diff options
Diffstat (limited to 'src/repl')
9 files changed, 405 insertions, 123 deletions
diff --git a/src/repl/scala/tools/nsc/interpreter/ILoop.scala b/src/repl/scala/tools/nsc/interpreter/ILoop.scala index 612bdd98c9..6721ff6dd6 100644 --- a/src/repl/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/repl/scala/tools/nsc/interpreter/ILoop.scala @@ -1,8 +1,7 @@ /* NSC -- new Scala compiler - * Copyright 2005-2013 LAMP/EPFL + * Copyright 2005-2015 LAMP/EPFL * @author Alexander Spoon */ - package scala package tools.nsc package interpreter @@ -24,9 +23,9 @@ import io.AbstractFile import scala.collection.generic.Clearable import scala.concurrent.{ ExecutionContext, Await, Future, future } import ExecutionContext.Implicits._ -import java.io.{ BufferedReader, FileReader } +import java.io.{ BufferedReader, FileReader, StringReader } -import scala.util.{Try, Success, Failure} +import scala.util.{ Try, Success, Failure } /** The Scala interactive shell. It provides a read-eval-print loop * around the Interpreter class. @@ -657,12 +656,9 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) unleashAndSetPhase() asyncEcho(isDuringInit, power.banner) } - private def unleashAndSetPhase() { - if (isReplPower) { - power.unleash() - // Set the phase to "typer" - intp beSilentDuring phaseCommand("typer") - } + private def unleashAndSetPhase() = if (isReplPower) { + power.unleash() + intp beSilentDuring phaseCommand("typer") // Set the phase to "typer" } def asyncEcho(async: Boolean, msg: => String) { @@ -750,28 +746,9 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) result } - private object paste extends Pasted { - import scala.util.matching.Regex.quote - val ContinuePrompt = replProps.continuePrompt - val ContinueString = replProps.continueText // " | " - val PromptString = prompt.lines.toList.last - val anyPrompt = s"""\\s*(?:${quote(PromptString.trim)}|${quote(AltPromptString.trim)})\\s*""".r - - def isPrompted(line: String) = matchesPrompt(line) - def isPromptOnly(line: String) = line match { case anyPrompt() => true ; case _ => false } - - def interpret(line: String): Unit = { - echo(line.trim) - intp interpret line - echo("") - } - - def transcript(start: String) = { - echo("\n// Detected repl transcript paste: ctrl-D to finish.\n") - apply(Iterator(start) ++ readWhile(!isPromptOnly(_))) - } - - def unapply(line: String): Boolean = isPrompted(line) + private object paste extends Pasted(prompt) { + def interpret(line: String) = intp interpret line + def echo(message: String) = ILoop.this echo message } private object invocation { @@ -786,30 +763,10 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) * read, go ahead and interpret it. Return the full string * to be recorded for replay, if any. */ - def interpretStartingWith(code: String): Option[String] = { + final def interpretStartingWith(code: String): Option[String] = { // signal completion non-completion input has been received in.completion.resetVerbosity() - def reallyInterpret = intp.interpret(code) match { - case IR.Error => None - case IR.Success => Some(code) - case IR.Incomplete if in.interactive && code.endsWith("\n\n") => - echo("You typed two blank lines. Starting a new command.") - None - case IR.Incomplete => - in.readLine(paste.ContinuePrompt) match { - case null => - // we know compilation is going to fail since we're at EOF and the - // parser thinks the input is still incomplete, but since this is - // a file being read non-interactively we want to fail. So we send - // it straight to the compiler for the nice error message. - intp.compileString(code) - None - - case line => interpretStartingWith(code + "\n" + line) - } - } - /* Here we place ourselves between the user and the interpreter and examine * the input they are ostensibly submitting. We intervene in several cases: * @@ -822,9 +779,33 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) code match { case "" => None case lineComment() => None // line comment, do nothing - case paste() if !paste.running => paste.transcript(code) ; None + case paste() if !paste.running => paste.transcript(Iterator(code) ++ readWhile(!paste.isPromptOnly(_))) match { + case Some(s) => interpretStartingWith(s) + case _ => None + } case invocation() if intp.mostRecentVar != "" => interpretStartingWith(intp.mostRecentVar + code) - case _ => reallyInterpret + case _ => intp.interpret(code) match { + case IR.Error => None + case IR.Success => Some(code) + case IR.Incomplete if in.interactive && code.endsWith("\n\n") => + echo("You typed two blank lines. Starting a new command.") + None + case IR.Incomplete => + val saved = intp.partialInput + intp.partialInput = code + "\n" + try { + in.readLine(paste.ContinuePrompt) match { + case null => + // we know compilation is going to fail since we're at EOF and the + // parser thinks the input is still incomplete, but since this is + // a file being read non-interactively we want to fail. So we send + // it straight to the compiler for the nice error message. + intp.compileString(code) + None + case line => interpretStartingWith(s"$code\n$line") + } + } finally intp.partialInput = saved + } } } @@ -840,9 +821,10 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) case _ => } - /** Tries to create a JLineReader, falling back to SimpleReader: - * unless settings or properties are such that it should start - * with SimpleReader. + /** Tries to create a JLineReader, falling back to SimpleReader, + * unless settings or properties are such that it should start with SimpleReader. + * The constructor of the InteractiveReader must take a Completion strategy, + * supplied as a `() => Completion`; the Completion object provides a concrete Completer. */ def chooseReader(settings: Settings): InteractiveReader = { if (settings.Xnojline || Properties.isEmacsShell) SimpleReader() @@ -850,20 +832,25 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) type Completer = () => Completion type ReaderMaker = Completer => InteractiveReader - def instantiate(className: String): ReaderMaker = completer => { - if (settings.debug) Console.println(s"Trying to instantiate a InteractiveReader from $className") + def instantiater(className: String): ReaderMaker = completer => { + if (settings.debug) Console.println(s"Trying to instantiate an InteractiveReader from $className") Class.forName(className).getConstructor(classOf[Completer]). newInstance(completer). asInstanceOf[InteractiveReader] } - def mkReader(maker: ReaderMaker) = - if (settings.noCompletion) maker(() => NoCompletion) - else maker(() => new JLineCompletion(intp)) // JLineCompletion is a misnomer -- it's not tied to jline + def mkReader(maker: ReaderMaker) = maker { () => + settings.completion.value match { + case _ if settings.noCompletion => NoCompletion + case "none" => NoCompletion + case "adhoc" => new JLineCompletion(intp) // JLineCompletion is a misnomer; it's not tied to jline + case "pc" | _ => new PresentationCompilerCompleter(intp) + } + } def internalClass(kind: String) = s"scala.tools.nsc.interpreter.$kind.InteractiveReader" val readerClasses = sys.props.get("scala.repl.reader").toStream ++ Stream(internalClass("jline"), internalClass("jline_embedded")) - val readers = readerClasses map (cls => Try { mkReader(instantiate(cls)) }) + val readers = readerClasses map (cls => Try { mkReader(instantiater(cls)) }) val reader = (readers collect { case Success(reader) => reader } headOption) getOrElse SimpleReader() diff --git a/src/repl/scala/tools/nsc/interpreter/IMain.scala b/src/repl/scala/tools/nsc/interpreter/IMain.scala index 3b54f5274e..ef6ab4063a 100644 --- a/src/repl/scala/tools/nsc/interpreter/IMain.scala +++ b/src/repl/scala/tools/nsc/interpreter/IMain.scala @@ -15,10 +15,13 @@ import scala.concurrent.{ Future, ExecutionContext } import scala.reflect.runtime.{ universe => ru } import scala.reflect.{ ClassTag, classTag } import scala.reflect.internal.util.{ BatchSourceFile, SourceFile } +import scala.tools.nsc.interactive +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.nsc.util.ClassPath.DefaultJavaContext import scala.tools.util.PathResolverFactory import scala.tools.nsc.io.AbstractFile import scala.tools.nsc.typechecker.{ TypeStrings, StructuredTypeStrings } -import scala.tools.nsc.util.{ ScalaClassLoader, stringFromReader, stringFromWriter, StackTraceOps, ClassPath, MergedClassPath } +import scala.tools.nsc.util._ import ScalaClassLoader.URLClassLoader import scala.tools.nsc.util.Exceptional.unwrap import scala.tools.nsc.backend.JavaPlatform @@ -58,7 +61,7 @@ import java.io.File * @author Moez A. Abdel-Gawad * @author Lex Spoon */ -class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Settings, protected val out: JPrintWriter) extends AbstractScriptEngine with Compilable with Imports { +class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Settings, protected val out: JPrintWriter) extends AbstractScriptEngine with Compilable with Imports with PresentationCompilation { imain => setBindings(createBindings, ScriptContext.ENGINE_SCOPE) @@ -77,6 +80,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set private var _isInitialized: Future[Boolean] = null // set up initialization future private var bindExceptions = true // whether to bind the lastException variable private var _executionWrapper = "" // code to be wrapped around all lines + var partialInput: String = "" // code accumulated in multi-line REPL input /** We're going to go to some trouble to initialize the compiler asynchronously. * It's critical that nothing call into it until it's been initialized or we will @@ -446,7 +450,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set /** Build a request from the user. `trees` is `line` after being parsed. */ - private def buildRequest(line: String, trees: List[Tree]): Request = { + private[interpreter] def buildRequest(line: String, trees: List[Tree]): Request = { executingRequest = new Request(line, trees) executingRequest } @@ -465,11 +469,12 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set pos } - private def requestFromLine(line: String, synthetic: Boolean): Either[IR.Result, Request] = { - val content = line //indentCode(line) - val trees = parse(content) match { - case parse.Incomplete => return Left(IR.Incomplete) - case parse.Error => return Left(IR.Error) + private[interpreter] def requestFromLine(line: String, synthetic: Boolean): Either[IR.Result, Request] = { + val content = line + + val trees: List[global.Tree] = parse(content) match { + case parse.Incomplete(_) => return Left(IR.Incomplete) + case parse.Error(_) => return Left(IR.Error) case parse.Success(trees) => trees } repltrace( @@ -854,7 +859,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set } /** One line of code submitted by the user for interpretation */ - class Request(val line: String, val trees: List[Tree]) { + class Request(val line: String, val trees: List[Tree], generousImports: Boolean = false) { def defines = defHandlers flatMap (_.definedSymbols) def imports = importedSymbols def value = Some(handlers.last) filter (h => h.definesValue) map (h => definedSymbols(h.definesTerm.get)) getOrElse NoSymbol @@ -888,7 +893,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set * append to objectName to access anything bound by request. */ lazy val ComputedImports(headerPreamble, importsPreamble, importsTrailer, accessPath) = - exitingTyper(importsCode(referencedNames.toSet, ObjectSourceCode, definesClass)) + exitingTyper(importsCode(referencedNames.toSet, ObjectSourceCode, definesClass, generousImports)) /** the line of code to compute */ def toCompute = line @@ -915,6 +920,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set |${envLines mkString (" ", ";\n ", ";\n")} |$importsPreamble |${indentCode(toCompute)}""".stripMargin + def preambleLength = preamble.length - toCompute.length - 1 val generate = (m: MemberHandler) => m extraCodeToEvaluate Request.this @@ -954,7 +960,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set def postwrap = s"}\nval $iw = new $iw\n" } - private lazy val ObjectSourceCode: Wrapper = + private[interpreter] lazy val ObjectSourceCode: Wrapper = if (isClassBased) new ClassBasedWrapper else new ObjectBasedWrapper private object ResultObjectSourceCode extends IMain.CodeAssembler[MemberHandler] { @@ -1015,6 +1021,7 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set } lazy val resultSymbol = lineRep.resolvePathToSymbol(fullAccessPath) + def applyToResultMember[T](name: Name, f: Symbol => T) = exitingTyper(f(resultSymbol.info.nonPrivateDecl(name))) /* typeOf lookup with encoding */ @@ -1168,20 +1175,22 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set /** Parse a line into and return parsing result (error, incomplete or success with list of trees) */ object parse { - abstract sealed class Result - case object Error extends Result - case object Incomplete extends Result + abstract sealed class Result { def trees: List[Tree] } + case class Error(trees: List[Tree]) extends Result + case class Incomplete(trees: List[Tree]) extends Result case class Success(trees: List[Tree]) extends Result def apply(line: String): Result = debugging(s"""parse("$line")""") { var isIncomplete = false - currentRun.parsing.withIncompleteHandler((_, _) => isIncomplete = true) { + def parse = { reporter.reset() val trees = newUnitParser(line).parseStats() - if (reporter.hasErrors) Error - else if (isIncomplete) Incomplete + if (reporter.hasErrors) Error(trees) + else if (isIncomplete) Incomplete(trees) else Success(trees) } + currentRun.parsing.withIncompleteHandler((_, _) => isIncomplete = true) {parse} + } } @@ -1264,6 +1273,8 @@ class IMain(@BeanProperty val factory: ScriptEngineFactory, initialSettings: Set /** Utility methods for the Interpreter. */ object IMain { import java.util.Arrays.{ asList => asJavaList } + /** Dummy identifier fragement inserted at the cursor before presentation compilation. Needed to support completion of `global.def<TAB>` */ + val DummyCursorFragment = "_CURSOR_" class Factory extends ScriptEngineFactory { @BeanProperty @@ -1361,3 +1372,4 @@ object IMain { def stripImpl(str: String): String = naming.unmangle(str) } } + diff --git a/src/repl/scala/tools/nsc/interpreter/Imports.scala b/src/repl/scala/tools/nsc/interpreter/Imports.scala index 5b231d94b6..5742c1d0d8 100644 --- a/src/repl/scala/tools/nsc/interpreter/Imports.scala +++ b/src/repl/scala/tools/nsc/interpreter/Imports.scala @@ -95,7 +95,8 @@ trait Imports { * last one imported is actually usable. */ case class ComputedImports(header: String, prepend: String, append: String, access: String) - protected def importsCode(wanted: Set[Name], wrapper: Request#Wrapper, definesClass: Boolean): ComputedImports = { + + protected def importsCode(wanted: Set[Name], wrapper: Request#Wrapper, definesClass: Boolean, generousImports: Boolean): ComputedImports = { val header, code, trailingBraces, accessPath = new StringBuilder val currentImps = mutable.HashSet[Name]() var predefEscapes = false // only emit predef import header if name not resolved in history, loosely @@ -116,8 +117,9 @@ trait Imports { def keepHandler(handler: MemberHandler) = handler match { // While defining classes in class based mode - implicits are not needed. case h: ImportHandler if isClassBased && definesClass => h.importedNames.exists(x => wanted.contains(x)) - case _: ImportHandler => true - case x => x.definesImplicit || (x.definedNames exists wanted) + case _: ImportHandler => true + case x if generousImports => x.definesImplicit || (x.definedNames exists (d => wanted.exists(w => d.startsWith(w)))) + case x => x.definesImplicit || (x.definedNames exists wanted) } reqs match { diff --git a/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala b/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala index d878988e26..e9b0234a4f 100644 --- a/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala +++ b/src/repl/scala/tools/nsc/interpreter/JLineCompletion.scala @@ -9,6 +9,7 @@ package interpreter import Completion._ import scala.collection.mutable.ListBuffer import scala.reflect.internal.util.StringOps.longestCommonPrefix +import scala.tools.nsc.interactive.Global // REPL completor - queries supplied interpreter for valid // completions based on current contents of buffer. @@ -296,7 +297,6 @@ class JLineCompletion(val intp: IMain) extends Completion with CompletionOutput override def complete(buf: String, cursor: Int): Candidates = { verbosity = if (isConsecutiveTabs(buf, cursor)) verbosity + 1 else 0 repldbg(f"%ncomplete($buf, $cursor%d) last = ($lastBuf, $lastCursor%d), verbosity: $verbosity") - // we don't try lower priority completions unless higher ones return no results. def tryCompletion(p: Parsed, completionFunction: Parsed => List[String]): Option[Candidates] = { val winners = completionFunction(p) diff --git a/src/repl/scala/tools/nsc/interpreter/MemberHandlers.scala b/src/repl/scala/tools/nsc/interpreter/MemberHandlers.scala index bcba7b6dfd..4e45f6d615 100644 --- a/src/repl/scala/tools/nsc/interpreter/MemberHandlers.scala +++ b/src/repl/scala/tools/nsc/interpreter/MemberHandlers.scala @@ -35,7 +35,16 @@ trait MemberHandlers { // XXX this is obviously inadequate but it's going to require some effort // to get right. if (name.toString startsWith "x$") () - else importVars += name + else { + importVars += name + // Needed to import `xxx` during line 2 of: + // scala> val xxx = "" + // scala> def foo: x<TAB> + if (name.endsWith(IMain.DummyCursorFragment)) { + val stripped = name.stripSuffix(IMain.DummyCursorFragment) + importVars += stripped + } + } case _ => super.traverse(ast) } } diff --git a/src/repl/scala/tools/nsc/interpreter/Pasted.scala b/src/repl/scala/tools/nsc/interpreter/Pasted.scala index f8d8c2ddb1..3a7eda1b77 100644 --- a/src/repl/scala/tools/nsc/interpreter/Pasted.scala +++ b/src/repl/scala/tools/nsc/interpreter/Pasted.scala @@ -15,19 +15,23 @@ package interpreter * a transcript should itself be pasteable and should achieve * the same result. */ -abstract class Pasted { - def interpret(line: String): Unit - def ContinueString: String - def PromptString: String - def AltPromptString: String = "scala> " +abstract class Pasted(prompt: String) { + def interpret(line: String): IR.Result + def echo(message: String): Unit - /* `testBoth` cannot be a val, as `Pasted` is inherited by `object paste` in ILoop, - which would cause `val testBoth` to be initialized before `val PromptString` was. + val PromptString = prompt.lines.toList.last + val AltPromptString = "scala> " + val ContinuePrompt = replProps.continuePrompt + val ContinueString = replProps.continueText // " | " + val anyPrompt = { + import scala.util.matching.Regex.quote + s"""\\s*(?:${quote(PromptString.trim)}|${quote(AltPromptString.trim)})\\s*""".r + } + + def isPrompted(line: String) = matchesPrompt(line) + def isPromptOnly(line: String) = line match { case anyPrompt() => true ; case _ => false } - object paste extends Pasted { - val PromptString = prompt.lines.toList.last - */ - private def testBoth = PromptString != AltPromptString + private val testBoth = PromptString != AltPromptString private val spacey = " \t".toSet def matchesPrompt(line: String) = matchesString(line, PromptString) || testBoth && matchesString(line, AltPromptString) @@ -91,13 +95,26 @@ abstract class Pasted { case _ => code } - def run(): Unit = { - println("// Replaying %d commands from transcript.\n" format cmds.size) - cmds foreach { cmd => - print(ActualPromptString) - interpret(cmd) - } + def interpreted(line: String) = { + echo(line.trim) + val res = interpret(line) + if (res != IR.Incomplete) echo("") + res + } + def incompletely(cmd: String) = { + print(ActualPromptString) + interpreted(cmd) == IR.Incomplete } + def run(): Option[String] = { + echo(s"// Replaying ${cmds.size} commands from transcript.\n") + cmds find incompletely + } + } + + // Run transcript and return incomplete line if any. + def transcript(lines: TraversableOnce[String]): Option[String] = { + echo("\n// Detected repl transcript. Paste more, or ctrl-D to finish.\n") + apply(lines) } /** Commands start on lines beginning with "scala>" and each successive @@ -105,9 +122,10 @@ abstract class Pasted { * Everything else is discarded. When the end of the transcript is spotted, * all the commands are replayed. */ - def apply(lines: TraversableOnce[String]) = { + def apply(lines: TraversableOnce[String]): Option[String] = { isRunning = true - try new PasteAnalyzer(lines.toList) run() + try new PasteAnalyzer(lines.toList).run() finally isRunning = false } + def unapply(line: String): Boolean = isPrompted(line) } diff --git a/src/repl/scala/tools/nsc/interpreter/Power.scala b/src/repl/scala/tools/nsc/interpreter/Power.scala index 8d8140b638..a14a60d216 100644 --- a/src/repl/scala/tools/nsc/interpreter/Power.scala +++ b/src/repl/scala/tools/nsc/interpreter/Power.scala @@ -113,10 +113,13 @@ class Power[ReplValsImpl <: ReplVals : ru.TypeTag: ClassTag](val intp: IMain, re } } - private def customBanner = replProps.powerBanner.option flatMap (f => io.File(f).safeSlurp()) + private def customBanner = replProps.powerBanner.option flatMap { + case f if f.getName == "classic" => Some(classic) + case f => io.File(f).safeSlurp() + } private def customInit = replProps.powerInitCode.option flatMap (f => io.File(f).safeSlurp()) - def banner = customBanner getOrElse """ + def classic = """ |** Power User mode enabled - BEEP WHIR GYVE ** |** :phase has been set to 'typer'. ** |** scala.tools.nsc._ has been imported ** @@ -124,28 +127,30 @@ class Power[ReplValsImpl <: ReplVals : ru.TypeTag: ClassTag](val intp: IMain, re |** Try :help, :vals, power.<tab> ** """.stripMargin.trim - private def initImports = List( - "scala.tools.nsc._", - "scala.collection.JavaConverters._", - "intp.global.{ error => _, _ }", - "definitions.{ getClass => _, _ }", - "power.rutil._", - "replImplicits._", - "treedsl.CODE._" - ) - - def init = customInit match { - case Some(x) => x - case _ => initImports.mkString("import ", ", ", "") - } + def banner = customBanner getOrElse """ + |Power mode enabled. :phase is at typer. + |import scala.tools.nsc._, intp.global._, definitions._ + |Try :help or completions for vals._ and power._ + """.stripMargin.trim + + private def initImports = + """scala.tools.nsc._ + |scala.collection.JavaConverters._ + |intp.global.{ error => _, _ } + |definitions.{ getClass => _, _ } + |power.rutil._ + |replImplicits._ + |treedsl.CODE._""".stripMargin.lines + + def init = customInit getOrElse initImports.mkString("import ", ", ", "") - /** Starts up power mode and runs whatever is in init. + /** Quietly starts up power mode and runs whatever is in init. */ def unleash(): Unit = beQuietDuring { // First we create the ReplVals instance and bind it to $r intp.bind("$r", replVals) // Then we import everything from $r. - intp interpret ("import " + intp.originalPath("$r") + "._") + intp interpret s"import ${ intp.originalPath("$r") }._" // And whatever else there is to do. init.lines foreach (intp interpret _) } diff --git a/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala b/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala new file mode 100644 index 0000000000..3a2177a4cb --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/PresentationCompilation.scala @@ -0,0 +1,110 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2015 LAMP/EPFL + * @author Martin Odersky + */ +package scala.tools.nsc.interpreter + +import scala.reflect.internal.util.RangePosition +import scala.tools.nsc.backend.JavaPlatform +import scala.tools.nsc.{interactive, Settings} +import scala.tools.nsc.io._ +import scala.tools.nsc.reporters.StoreReporter +import scala.tools.nsc.util.ClassPath.DefaultJavaContext +import scala.tools.nsc.util.{DirectoryClassPath, MergedClassPath} + +trait PresentationCompilation { + self: IMain => + + /** 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 + * suggestions. + * + * The caller is responsible for calling [[PresentationCompileResult#cleanup]] to dispose of the compiler instance. + */ + private[scala] def presentationCompile(line: String): Either[IR.Result, PresentationCompileResult] = { + if (global == null) Left(IR.Error) + else { + // special case for: + // + // scala> 1 + // scala> .toInt + // + // and for multi-line input. + val line1 = partialInput + (if (Completion.looksLikeInvocation(line)) { self.mostRecentVar + line } else line) + val compiler = newPresentationCompiler() + val trees = compiler.newUnitParser(line1).parseStats() + val importer = global.mkImporter(compiler) + val request = new Request(line1, trees map (t => importer.importTree(t)), generousImports = true) + val wrappedCode: String = request.ObjectSourceCode(request.handlers) + val unit = compiler.newCompilationUnit(wrappedCode) + import compiler._ + val richUnit = new RichCompilationUnit(unit.source) + unitOfFile(richUnit.source.file) = richUnit + enteringTyper(typeCheck(richUnit)) + val result = PresentationCompileResult(compiler)(richUnit, request.ObjectSourceCode.preambleLength + line1.length - line.length) + Right(result) + } + } + + /** Create an instance of the presentation compiler with a classpath comprising the REPL's configured classpath + * and the classes output by previously compiled REPL lines. + * + * You may directly interact with this compiler from any thread, although you must not access it concurrently + * from multiple threads. + * + * You may downcast the `reporter` to `StoreReporter` to access type errors. + */ + def newPresentationCompiler(): interactive.Global = { + val replOutClasspath: DirectoryClassPath = new DirectoryClassPath(replOutput.dir, DefaultJavaContext) + val mergedClasspath = new MergedClassPath[AbstractFile](replOutClasspath :: global.platform.classPath :: Nil, DefaultJavaContext) + def copySettings: Settings = { + val s = new Settings(_ => () /* ignores "bad option -nc" errors, etc */) + s.processArguments(global.settings.recreateArgs, processAll = false) + s.YpresentationAnyThread.value = true + s + } + val storeReporter: StoreReporter = new StoreReporter + val interactiveGlobal = new interactive.Global(copySettings, storeReporter) { self => + override lazy val platform: ThisPlatform = new JavaPlatform { + val global: self.type = self + + override def classPath: PlatformClassPath = mergedClasspath + } + } + new interactiveGlobal.TyperRun() + interactiveGlobal + } + + abstract class PresentationCompileResult { + val compiler: scala.tools.nsc.interactive.Global + def unit: compiler.RichCompilationUnit + /** The length of synthetic code the precedes the user writtn code */ + def preambleLength: Int + def cleanup(): Unit = { + compiler.askShutdown() + } + import compiler.CompletionResult + + def completionsAt(cursor: Int): CompletionResult = { + val pos = unit.source.position(preambleLength + cursor) + compiler.completionsAt(pos) + } + def typedTreeAt(code: String, selectionStart: Int, selectionEnd: Int): compiler.Tree = { + val start = selectionStart + preambleLength + val end = selectionEnd + preambleLength + val pos = new RangePosition(unit.source, start, start, end) + compiler.typedTreeAt(pos) + } + } + + object PresentationCompileResult { + def apply(compiler0: interactive.Global)(unit0: compiler0.RichCompilationUnit, preambleLength0: Int) = new PresentationCompileResult { + + override val compiler = compiler0 + + override def unit = unit0.asInstanceOf[compiler.RichCompilationUnit] + + override def preambleLength = preambleLength0 + } + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/PresentationCompilerCompleter.scala b/src/repl/scala/tools/nsc/interpreter/PresentationCompilerCompleter.scala new file mode 100644 index 0000000000..0fb3236966 --- /dev/null +++ b/src/repl/scala/tools/nsc/interpreter/PresentationCompilerCompleter.scala @@ -0,0 +1,139 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2013 LAMP/EPFL + * @author Martin Odersky + */ +package scala.tools.nsc.interpreter + +import scala.reflect.internal.Flags +import scala.reflect.internal.util.StringOps +import scala.tools.nsc.interpreter.Completion.{ScalaCompleter, Candidates} +import scala.util.control.NonFatal + +class PresentationCompilerCompleter(intp: IMain) extends Completion with ScalaCompleter { + import PresentationCompilerCompleter._ + import intp.{PresentationCompileResult => Result} + + private type Handler = Result => Candidates + + private var lastRequest = NoRequest + private var tabCount = 0 + private var lastCommonPrefixCompletion: Option[String] = None + + def resetVerbosity(): Unit = { tabCount = 0 ; lastRequest = NoRequest } + def completer(): ScalaCompleter = this + + // A convenience for testing + def complete(before: String, after: String = ""): Candidates = complete(before + after, before.length) + override def complete(buf: String, cursor: Int): Candidates = { + val request = Request(buf, cursor) + if (request == lastRequest) + tabCount += 1 + else { + tabCount = 0 + lastRequest = request + } + + // secret handshakes + val slashPrint = """.*// *print *""".r + val slashTypeAt = """.*// *typeAt *(\d+) *(\d+) *""".r + val Cursor = IMain.DummyCursorFragment + + def print(result: Result) = { + val offset = result.preambleLength + val pos1 = result.unit.source.position(offset).withEnd(offset + buf.length) + import result.compiler._ + val tree = new Locator(pos1) locateIn result.unit.body match { + case Template(_, _, constructor :: (rest :+ last)) => if (rest.isEmpty) last else Block(rest, last) + case t => t + } + val printed = showCode(tree) + " // : " + tree.tpe.safeToString + Candidates(cursor, "" :: printed :: Nil) + } + def typeAt(result: Result, start: Int, end: Int) = { + val tpString = result.compiler.exitingTyper(result.typedTreeAt(buf, start, end).tpe.toString) + Candidates(cursor, "" :: tpString :: Nil) + } + def candidates(result: Result): Candidates = { + import result.compiler._ + import CompletionResult._ + def defStringCandidates(matching: List[Member], name: Name): Candidates = { + val defStrings = for { + member <- matching + if member.symNameDropLocal == name + sym <- member.sym.alternatives + sugared = sym.sugaredSymbolOrSelf + } yield { + val tp = member.prefix memberType sym + sugared.defStringSeenAs(tp) + } + Candidates(cursor, "" :: defStrings.distinct) + } + val found = result.completionsAt(cursor) match { + case NoResults => Completion.NoCandidates + case r => + def shouldHide(m: Member): Boolean = { + val isUniversal = definitions.isUniversalMember(m.sym) + def viaUniversalExtensionMethod = m match { + case t: TypeMember if t.implicitlyAdded && t.viaView.info.params.head.info.bounds.isEmptyBounds => true + case _ => false + } + ( + isUniversal && nme.isReplWrapperName(m.prefix.typeSymbol.name) + || isUniversal && tabCount == 0 && r.name.isEmpty + || viaUniversalExtensionMethod && tabCount == 0 && r.name.isEmpty + ) + } + + val matching = r.matchingResults().filterNot(shouldHide) + 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(shouldHide) + 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 { + // regular completion + val memberCompletions: List[String] = matching.map(_.symNameDropLocal.decoded).distinct.sorted + Candidates(cursor - r.positionDelta, memberCompletions) + } + } + lastCommonPrefixCompletion = + if (found != Completion.NoCandidates && buf.length >= found.cursor) + Some(buf.substring(0, found.cursor) + StringOps.longestCommonPrefix(found.candidates)) + else + None + found + } + val buf1 = buf.patch(cursor, Cursor, 0) + try { + intp.presentationCompile(buf1) match { + case Left(_) => Completion.NoCandidates + case Right(result) => try { + buf match { + case slashPrint() if cursor == buf.length => print(result) + case slashTypeAt(start, end) if cursor == buf.length => typeAt(result, start.toInt, end.toInt) + case _ => candidates(result) + } + } finally result.cleanup() + } + } catch { + case NonFatal(e) => + if (isReplDebug) e.printStackTrace() + Completion.NoCandidates + } + } +} +object PresentationCompilerCompleter { + private case class Request(line: String, cursor: Int) + private val NoRequest = Request("", -1) +} |