diff options
author | Paul Phillips <paulp@improving.org> | 2010-01-23 20:30:01 +0000 |
---|---|---|
committer | Paul Phillips <paulp@improving.org> | 2010-01-23 20:30:01 +0000 |
commit | a0c0f0949797a563c9583d0f82c0f390a999ec7d (patch) | |
tree | 59a21db321f764372f3ad85930c527f7a1571f0e | |
parent | bb6e5958e63e3d70cd1f1a86f3fa0b5f3b670d8a (diff) | |
download | scala-a0c0f0949797a563c9583d0f82c0f390a999ec7d.tar.gz scala-a0c0f0949797a563c9583d0f82c0f390a999ec7d.tar.bz2 scala-a0c0f0949797a563c9583d0f82c0f390a999ec7d.zip |
Another big REPL patch.
a proper commit message, I will just say it adds a couple of pretty
frabjous features, in addition to cleaning up a whole bunch of
questionable code.
* Tab-completion now chains through intermediate results on fields and 0-arg methods
* You can now define custom Completors which define their own contents.
Details and demos to come in a wiki document about the repl.
14 files changed, 753 insertions, 505 deletions
diff --git a/src/compiler/scala/tools/nsc/Interpreter.scala b/src/compiler/scala/tools/nsc/Interpreter.scala index d8cf729d25..01ed5087ae 100644 --- a/src/compiler/scala/tools/nsc/Interpreter.scala +++ b/src/compiler/scala/tools/nsc/Interpreter.scala @@ -12,9 +12,11 @@ import java.net.{ MalformedURLException, URL } import java.lang.reflect import reflect.InvocationTargetException +import scala.PartialFunction.{ cond, condOpt } import scala.reflect.Manifest import scala.collection.mutable -import scala.collection.mutable.{ ListBuffer, HashSet, ArrayBuffer } +import scala.collection.mutable.{ ListBuffer, HashSet, HashMap, ArrayBuffer } +import scala.collection.immutable.Set import scala.tools.nsc.util.ScalaClassLoader import ScalaClassLoader.URLClassLoader import scala.util.control.Exception.{ Catcher, catching, ultimately, unwrapping } @@ -68,8 +70,7 @@ import Interpreter._ * @author Moez A. Abdel-Gawad * @author Lex Spoon */ -class Interpreter(val settings: Settings, out: PrintWriter) -{ +class Interpreter(val settings: Settings, out: PrintWriter) { /** directory to save .class files to */ val virtualDirectory = new VirtualDirectory("(memory)", None) @@ -81,7 +82,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) import compiler.{ Tree, TermTree, ValOrDefDef, ValDef, DefDef, Assign, ClassDef, ModuleDef, Ident, Select, TypeDef, Import, MemberDef, DocDef, - EmptyTree } + ImportSelector, EmptyTree } import compiler.{ nme, newTermName } import nme.{ INTERPRETER_VAR_PREFIX, INTERPRETER_SYNTHVAR_PREFIX, INTERPRETER_LINE_PREFIX, @@ -193,11 +194,27 @@ class Interpreter(val settings: Settings, out: PrintWriter) /** the previous requests this interpreter has processed */ private val prevRequests = new ArrayBuffer[Request]() - val prevImports = new ListBuffer[Import]() + private val prevNameMap = new HashMap[Name, Request]() + private val boundNameMap = new HashMap[Name, Request]() - private def allUsedNames = prevRequests.toList.flatMap(_.usedNames).removeDuplicates - private def allBoundNames = prevRequests.toList.flatMap(_.boundNames).removeDuplicates - // private def allImportedNames = prevImports.toList.flatMap(_.importedNames).removeDuplicates + def recordRequest(req: Request) { + def tripart[T](set1: Set[T], set2: Set[T]) = { + val intersect = set1 intersect set2 + List(set1 -- intersect, intersect, set2 -- intersect) + } + + prevRequests += req + req.usedNames foreach (x => prevNameMap(x) = req) + req.boundNames foreach (x => boundNameMap(x) = req) + + // println("\n s1 = %s\n s2 = %s\n s3 = %s".format( + // tripart(prevNameMap.keysIterator.toSet, boundNameMap.keysIterator.toSet): _* + // )) + } + + private def mostRecentHandler = prevRequests.last.handlers.last + def allUsedNames = prevNameMap.keysIterator.toList + def allBoundNames = prevRequests.toList.flatMap(_.boundNames).removeDuplicates /** Generates names pre0, pre1, etc. via calls to apply method */ class NameCreator(pre: String) { @@ -231,6 +248,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) /** Check if a name looks like it was generated by varNameCreator */ private def isGeneratedVarName(name: String): Boolean = varNameCreator didGenerate name private def isSynthVarName(name: String): Boolean = synthVarNameCreator didGenerate name + def getVarName = varNameCreator() /** generate a string using a routine that wants to write on a stream */ private def stringFrom(writer: PrintWriter => Unit): String = { @@ -302,25 +320,26 @@ class Interpreter(val settings: Settings, out: PrintWriter) * should be taken. Removes requests which cannot contribute * useful imports for the specified set of wanted names. */ - case class ReqAndHandler(req: Request, handler: MemberHandler) + case class ReqAndHandler(req: Request, handler: MemberHandler) { } + def reqsToUse: List[ReqAndHandler] = { /** Loop through a list of MemberHandlers and select which ones to keep. * 'wanted' is the set of names that need to be imported. */ def select(reqs: List[ReqAndHandler], wanted: Set[Name]): List[ReqAndHandler] = { val isWanted = wanted contains _ - def keepHandler(handler: MemberHandler): Boolean = { - import handler._ - // Single symbol imports might be implicits! See bug #1752. Rather than - // try to finesse this, we will mimic all imports for now. - def isImport = handler.isInstanceOf[ImportHandler] - definesImplicit || isImport || (importedNames ++ boundNames).exists(isWanted) + // Single symbol imports might be implicits! See bug #1752. Rather than + // try to finesse this, we will mimic all imports for now. + def keepHandler(handler: MemberHandler) = handler match { + case _: ImportHandler => true + case x => x.definesImplicit || (x.boundNames exists isWanted) } reqs match { case Nil => Nil case rh :: rest if !keepHandler(rh.handler) => select(rest, wanted) case rh :: rest => + val importedNames = rh.handler match { case x: ImportHandler => x.importedNames ; case _ => Nil } import rh.handler._ val newWanted = wanted ++ usedNames -- boundNames -- importedNames rh :: select(rest, newWanted) @@ -352,32 +371,33 @@ class Interpreter(val settings: Settings, out: PrintWriter) // loop through previous requests, adding imports for each one for (ReqAndHandler(req, handler) <- reqsToUse) { - import handler._ - // If the user entered an import, then just use it; add an import wrapping - // level if the import might conflict with some other import - if (importsWildcard || currentImps.exists(importedNames.contains)) - addWrapper() - - if (member.isInstanceOf[Import]) - code append (member.toString + "\n") - - // give wildcard imports a import wrapper all to their own - if (importsWildcard) addWrapper() - else currentImps ++= importedNames - - // For other requests, import each bound variable. - // import them explicitly instead of with _, so that - // ambiguity errors will not be generated. Also, quote - // the name of the variable, so that we don't need to - // handle quoting keywords separately. - for (imv <- boundNames) { - if (currentImps contains imv) addWrapper() - - code append ("import " + req.fullPath(imv)) - currentImps += imv + handler match { + // If the user entered an import, then just use it; add an import wrapping + // level if the import might conflict with some other import + case x: ImportHandler => + if (x.importsWildcard || (currentImps exists (x.importedNames contains _))) + addWrapper() + + code append (x.member.toString + "\n") + + // give wildcard imports a import wrapper all to their own + if (x.importsWildcard) addWrapper() + else currentImps ++= x.importedNames + + // For other requests, import each bound variable. + // import them explicitly instead of with _, so that + // ambiguity errors will not be generated. Also, quote + // the name of the variable, so that we don't need to + // handle quoting keywords separately. + case x => + for (imv <- x.boundNames) { + if (currentImps contains imv) addWrapper() + + code append ("import " + (req fullPath imv)) + currentImps += imv + } } } - // add one extra wrapper, to prevent warnings in the common case of // redefining the value bound in the last interpreter request. addWrapper() @@ -457,7 +477,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) // modify code at a textual level, and it being hard to submit an AST to the compiler. if (trees.size == 1) trees.head match { case _:Assign => // we don't want to include assignments - case _:TermTree | _:Ident | _:Select => + case _:TermTree | _:Ident | _:Select => // ... but do want these as valdefs. return requestFromLine("val %s =\n%s".format(varNameCreator(), line)) case _ => } @@ -485,8 +505,8 @@ class Interpreter(val settings: Settings, out: PrintWriter) case Left(result) => return result case Right(req) => req } - - // null is a disallowed statement type; otherwise compile and fail if false (implying e.g. a type error) + // null indicates a disallowed statement type; otherwise compile and + // fail if false (implying e.g. a type error) if (req == null || !req.compile) return IR.Error @@ -495,7 +515,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) out print clean(result) if (succeeded) { - prevRequests += req // book-keeping + recordRequest(req) // book-keeping IR.Success } else IR.Error @@ -513,7 +533,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) * @return an indication of whether the binding succeeded */ def bind(name: String, boundType: String, value: Any): IR.Result = { - val binderName = newBinder() // "binder" + binderNum() + val binderName = newBinder() compileString(""" | object %s { @@ -576,13 +596,9 @@ class Interpreter(val settings: Settings, out: PrintWriter) def boundNames: List[Name] = Nil def valAndVarNames: List[Name] = Nil def defNames: List[Name] = Nil - val importsWildcard = false - val importedNames: Seq[Name] = Nil - val definesImplicit = member match { - case tree: MemberDef => tree.mods hasFlag Flags.IMPLICIT - case _ => false + val definesImplicit = cond(member) { + case tree: MemberDef => tree.mods hasFlag Flags.IMPLICIT } - def generatesValue: Option[Name] = None def extraCodeToEvaluate(req: Request, code: PrintWriter) { } @@ -681,18 +697,15 @@ class Interpreter(val settings: Settings, out: PrintWriter) private class ImportHandler(imp: Import) extends MemberHandler(imp) { /** Whether this import includes a wildcard import */ - override val importsWildcard = imp.selectors.map(_.name) contains USCOREkw + val importsWildcard = imp.selectors map (_.name) contains USCOREkw /** The individual names imported by this statement */ - override val importedNames: Seq[Name] = for { - sel <- imp.selectors - if (sel.rename != null && sel.rename != USCOREkw) - name <- List(sel.rename.toTypeName, sel.rename.toTermName) - } - yield name - - // record the import - prevImports += imp + val importedNames: List[Name] = ( + imp.selectors + . map (x => x.rename) + . filter (x => x != null && x != USCOREkw) + . flatMap (x => List(x.toTypeName, x.toTermName)) + ) override def resultExtractionCode(req: Request, code: PrintWriter) = code println codegenln(imp.toString) @@ -700,8 +713,6 @@ class Interpreter(val settings: Settings, out: PrintWriter) /** One line of code submitted by the user for interpretation */ private class Request(val line: String, val lineName: String, val trees: List[Tree]) { - // val trees = parse(line) getOrElse Nil - /** name to use for the object that will compute "line" */ def objectName = lineName + INTERPRETER_WRAPPER_SUFFIX @@ -718,7 +729,8 @@ class Interpreter(val settings: Settings, out: PrintWriter) val usedNames: List[Name] = handlers.flatMap(_.usedNames) /** Code to import bound names from previous lines - accessPath is code to - * append to objectName to access anything bound by request. */ + * append to objectName to access anything bound by request. + */ val ComputedImports(importsPreamble, importsTrailer, accessPath) = importsCode(Set.empty ++ usedNames) @@ -733,7 +745,6 @@ class Interpreter(val settings: Settings, out: PrintWriter) /** generate the source code for the object that computes this request */ def objectSourceCode: String = stringFrom { code => - // whitespace compatible with interpreter.scala val preamble = """object %s { | %s%s """.stripMargin.format(objectName, importsPreamble, indentCode(toCompute)) @@ -792,17 +803,14 @@ class Interpreter(val settings: Settings, out: PrintWriter) x.compileSources(List(new BatchSourceFile("<console>", resultObjectSourceCode))) x } + lazy val loadedResultObject = loadByName(resultObjectName) - def extractionValue(): Option[AnyRef] = { + def extractionValue(): AnyRef = { // ensure it has run extractionObjectRun - catching(classOf[Exception]) opt { - // load it and retrieve the value - val result: Class[_] = loadByName(resultObjectName) - - result getMethod "scala_repl_value" invoke result - } + // load it and retrieve the value + loadedResultObject getMethod "scala_repl_value" invoke loadedResultObject } /** Compile the object file. Returns whether the compilation succeeded. @@ -866,8 +874,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) /** load and run the code using reflection */ def loadAndRun: (String, Boolean) = { - val resultObject: Class[_] = loadByName(resultObjectName) - val resultValMethod: reflect.Method = resultObject getMethod "scala_repl_result" + val resultValMethod: reflect.Method = loadedResultObject getMethod "scala_repl_result" // XXX if wrapperExceptions isn't type-annotated we crash scalac val wrapperExceptions: List[Class[_ <: Throwable]] = List(classOf[InvocationTargetException], classOf[ExceptionInInitializerError]) @@ -883,7 +890,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) catching(onErr) { unwrapping(wrapperExceptions: _*) { - (resultValMethod.invoke(resultObject).toString, true) + (resultValMethod.invoke(loadedResultObject).toString, true) } } } @@ -893,10 +900,6 @@ class Interpreter(val settings: Settings, out: PrintWriter) * The command infrastructure is in InterpreterLoop. */ def dumpState(xs: List[String]): String = { - // println("Imports for " + req + " => " + req.importsPreamble) - // req.handlers foreach { h => println("Handler " + h + " used names: " + h.usedNames) } - // req.trees foreach { x => println("Tree: " + x) } - // xs foreach { x => println("membersOfIdentifier(" + x + ") = " + membersOfIdentifier(x)) } List( "allUsedNames = " + allUsedNames, "allBoundNames = " + allBoundNames, @@ -927,43 +930,42 @@ class Interpreter(val settings: Settings, out: PrintWriter) |** mkTree("def f(x: Int, y: Int) = x+y") **""".stripMargin } - def nameOfIdent(line: String): Option[Name] = { - parse(line) match { - case Some(List(Ident(x))) => Some(x) - case _ => None - } - } - /** Returns the name of the most recent interpreter result. * Mostly this exists so you can conveniently invoke methods on * the previous result. */ - def mostRecentVar: String = - prevRequests.last.handlers.last.member match { - case x: ValOrDefDef => x.name - case Assign(Ident(name), _) => name - case ModuleDef(_, name, _) => name - case _ => varNameCreator.mostRecent - } - - private def requestForName(name: Name): Option[Request] = { - for (req <- prevRequests.toList.reverse) { - if (req.handlers.exists(_.boundNames contains name)) - return Some(req) - } - None + def mostRecentVar: String = mostRecentHandler.member match { + case x: ValOrDefDef => x.name + case Assign(Ident(name), _) => name + case ModuleDef(_, name, _) => name + case _ => varNameCreator.mostRecent } + + private def requestForName(name: Name): Option[Request] = + prevRequests.reverse find (_.boundNames contains name) + private def requestForIdent(line: String): Option[Request] = - nameOfIdent(line) flatMap requestForName + requestForName(newTermName(line)) + + def methodsOf(name: String) = + evalExpr[List[String]](methodsCode(name)) map (x => NameTransformer.decode(getOriginalName(x))) + + def completionAware(name: String) = + evalExpr[Option[CompletionAware]](asCompletionAwareCode(name)) + + def extractionValueForIdent(id: String): AnyRef = + requestForIdent(id).get.extractionValue + + def implicitFor[T: Manifest] = evalExpr[T]("""manifest[%s]""".format(manifest[T])) - private def mkValDef(line: String, name: String = varNameCreator()) = - (name, "val %s = %s".format(name, line)) + def clazzForIdent(id: String): Class[_] = + extractionValueForIdent(id).getClass - // private def inCompletion(s: String) = "scala.tools.nsc.interpreter.Completion." + s - private def inCompletion(s: String) = classOf[Completion].getName + "." + s - private def methodsCode(name: String) = inCompletion("methodsOf(" + name + ")") - private def isSpecialCode(name: String) = inCompletion("isSpecial(" + name + ")") - private def selfDefinedMembersCode(name: String) = inCompletion("selfDefinedMembers(" + name + ")") + private def methodsCode(name: String) = + "%s.%s(%s)".format(classOf[ReflectionCompletion].getName, "methodsOf", name) + + private def asCompletionAwareCode(name: String) = + "%s.%s(%s)".format(classOf[CompletionAware].getName, "unapply", name) private def getOriginalName(name: String): String = nme.originalName(newTermName(name)).toString @@ -996,7 +998,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) if (req == null || !req.compile || req.handlers.size != 1) evalError("Eval error.") - try req.extractionValue.get.asInstanceOf[T] catch { + try req.extractionValue.asInstanceOf[T] catch { case e: Exception => evalError(e.getMessage) } } @@ -1004,33 +1006,12 @@ class Interpreter(val settings: Settings, out: PrintWriter) def interpretExpr[T: Manifest](code: String): Option[T] = beQuietDuring { interpret(code) match { case IR.Success => - try Some(prevRequests.last.extractionValue.get.asInstanceOf[T]) + try Some(prevRequests.last.extractionValue.asInstanceOf[T]) catch { case e: Exception => println(e) ; None } case _ => None } } - private def memberListFor(name: String): List[String] = { - import NameTransformer.{ decode, encode } // e.g. $plus$plus => ++ - - /** Give objects a chance to define their own members. */ - val special = evalExpr[Option[List[String]]](selfDefinedMembersCode(name)) - - /** Failing that, use reflection. */ - special getOrElse evalExpr[List[String]](methodsCode(name)) map (x => decode(getOriginalName(x))) - } - - /** The main entry point for tab-completion. When the user types x.<tab> - * this method is called with "x" as an argument, and it discovers the - * fields and methods of x via reflection and returns their names to jline. - */ - def membersOfIdentifier(line: String): List[String] = - beQuietDuring { - for (name <- nameOfIdent(line) ; req <- requestForName(name)) yield { - memberListFor(name) filterNot Completion.shouldHide removeDuplicates - } - } getOrElse Nil - /** Another entry point for tab-completion, ids in scope */ def unqualifiedIds(): List[String] = allBoundNames map (_.toString) filterNot isSynthVarName @@ -1042,8 +1023,11 @@ class Interpreter(val settings: Settings, out: PrintWriter) def aliasForType(path: String) = ByteCode.aliasForType(path) /** Artificial object */ - class ReplVars extends Completion.Special { - def tabCompletions() = unqualifiedIds() + class ReplVars extends CompletionAware { + def completions() = unqualifiedIds() + override def follow(s: String) = + if (completions contains s) completionAware(s) + else None } def replVarsObject() = new ReplVars() @@ -1066,16 +1050,16 @@ class Interpreter(val settings: Settings, out: PrintWriter) object Interpreter { object DebugParam { - implicit def tuple2debugparam[T](x: (String, T))(implicit m: scala.reflect.Manifest[T]): DebugParam[T] = + implicit def tuple2debugparam[T](x: (String, T))(implicit m: Manifest[T]): DebugParam[T] = DebugParam(x._1, x._2) - implicit def any2debugparam[T](x: T)(implicit m: scala.reflect.Manifest[T]): DebugParam[T] = + implicit def any2debugparam[T](x: T)(implicit m: Manifest[T]): DebugParam[T] = DebugParam("p" + getCount(), x) private var counter = 0 def getCount() = { counter += 1; counter } } - case class DebugParam[T](name: String, param: T)(implicit m: scala.reflect.Manifest[T]) { + case class DebugParam[T](name: String, param: T)(implicit m: Manifest[T]) { val manifest = m val typeStr = { val str = manifest.toString diff --git a/src/compiler/scala/tools/nsc/InterpreterLoop.scala b/src/compiler/scala/tools/nsc/InterpreterLoop.scala index 9cc372097a..2d8a5b78ee 100644 --- a/src/compiler/scala/tools/nsc/InterpreterLoop.scala +++ b/src/compiler/scala/tools/nsc/InterpreterLoop.scala @@ -78,14 +78,12 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { /** The input stream from which commands come, set by main() */ var in: InteractiveReader = _ - def historyList = in.history map (_.asList) getOrElse Nil /** The context class loader at the time this object was created */ protected val originalClassLoader = Thread.currentThread.getContextClassLoader var settings: Settings = _ // set by main() var interpreter: Interpreter = _ // set by createInterpreter() - def isettings = interpreter.isettings // XXX var addedClasspath: List[String] = Nil @@ -119,15 +117,6 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { interpreter.setContextClassLoader() } - /** Bind the settings so that evaluated code can modify them */ - def bindSettings() { - isettings - // interpreter.beQuietDuring { - // interpreter.compileString(InterpreterSettings.sourceCodeForClass) - // interpreter.bind("settings", "scala.tools.nsc.InterpreterSettings", isettings) - // } - } - /** print a friendly help message */ def printHelp() = { out println "All commands can be abbreviated - for example :he instead of :help.\n" @@ -159,7 +148,7 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { val current = in.history.get.index val count = try xs.head.toInt catch { case _: Exception => defaultLines } - val lines = historyList takeRight count + val lines = in.historyList takeRight count val offset = current - lines.size + 1 for ((line, index) <- lines.zipWithIndex) @@ -174,9 +163,9 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { return println("No history available.") val current = in.history.get.index - val offset = current - historyList.size + 1 + val offset = current - in.historyList.size + 1 - for ((line, index) <- historyList.zipWithIndex ; if line.toLowerCase contains cmdline) + for ((line, index) <- in.historyList.zipWithIndex ; if line.toLowerCase contains cmdline) println("%d %s".format(index + offset, line)) } @@ -249,12 +238,13 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { /* For some reason, the first interpreted command always takes * a second or two. So, wait until the welcome message - * has been printed before calling bindSettings. That way, + * has been printed before calling isettings. That way, * the user can read the welcome message while this * command executes. */ val futLine = scala.concurrent.ops.future(readOneLine) - bindSettings() + interpreter.isettings // evaluates lazy val + if (!processLine(futLine())) return @@ -339,7 +329,7 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { powerUserOn = true out println interpreter.powerUser() if (in.history.isDefined) - interpreter.quietBind("history", "scala.collection.immutable.List[String]", historyList) + interpreter.quietBind("history", "scala.collection.immutable.List[String]", in.historyList) interpreter.quietBind("repl", "scala.tools.nsc.Interpreter#ReplVars", interpreter.replVarsObject()) } @@ -384,21 +374,38 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { * read, go ahead and interpret it. Return the full string * to be recorded for replay, if any. */ - def interpretStartingWith(code: String): Option[String] = - if (code startsWith ".") interpretStartingWith(interpreter.mostRecentVar + code) - else interpreter.interpret(code) match { - case IR.Error => None - case IR.Success => Some(code) - case IR.Incomplete => - if (in.interactive && code.endsWith("\n\n")) { - out.println("You typed two blank lines. Starting a new command.") - None - } - else in.readLine(" | ") match { - case null => None // end of file - case line => interpretStartingWith(code + "\n" + line) - } + def interpretStartingWith(code: String): Option[String] = { + def reallyInterpret = { + interpreter.interpret(code) match { + case IR.Error => None + case IR.Success => Some(code) + case IR.Incomplete => + if (in.interactive && code.endsWith("\n\n")) { + out.println("You typed two blank lines. Starting a new command.") + None + } + else in.readLine(" | ") match { + case null => None // end of file + case line => interpretStartingWith(code + "\n" + line) + } + } + } + + /** Here we place ourselves between the user and the interpreter and examine + * the input they are ostensibly submitting. It may turn out to be the result + * of tab-completion, in which case it might be meaningless to scala but + * evaluable by the CompletionAware unit which created it. + */ + if (code == "") None + else if (code startsWith ".") interpretStartingWith(interpreter.mostRecentVar + code) + else { + val result = for (comp <- in.completion ; res <- comp execute code) yield res + result match { + case Some(res) => injectAndName(res) ; None // completion took responsibility, so do not parse + case _ => reallyInterpret + } } + } // runs :load <file> on any files passed via -i def loadFiles(settings: Settings) = settings match { @@ -425,7 +432,7 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { // the interpeter is passed as an argument to expose tab completion info if (settings.Xnojline.value || emacsShell) new SimpleReader else if (settings.noCompletion.value) InteractiveReader.createDefault() - else InteractiveReader.createDefault(interpreter, this) + else InteractiveReader.createDefault(interpreter) } loadFiles(settings) @@ -435,20 +442,26 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { printWelcome() repl() - } finally { - closeInterpreter() - } + } finally closeInterpreter() } + private def objName(x: Any) = x.asInstanceOf[AnyRef].getClass.getName + // injects one value into the repl; returns pair of name and class def injectOne(name: String, obj: Any): Tuple2[String, String] = { - val className = obj.asInstanceOf[AnyRef].getClass.getName + val className = objName(obj) interpreter.quietBind(name, className, obj) (name, className) } + def injectAndName(obj: Any): Tuple2[String, String] = { + val name = interpreter.getVarName + val className = objName(obj) + interpreter.bind(name, className, obj) + (name, className) + } // injects list of values into the repl; returns summary string - def inject(args: List[Any]): String = { + def injectDebug(args: List[Any]): String = { val strs = for ((arg, i) <- args.zipWithIndex) yield { val varName = "p" + (i + 1) diff --git a/src/compiler/scala/tools/nsc/interpreter/Completion.scala b/src/compiler/scala/tools/nsc/interpreter/Completion.scala index 3e6698b605..dc544ed657 100644 --- a/src/compiler/scala/tools/nsc/interpreter/Completion.scala +++ b/src/compiler/scala/tools/nsc/interpreter/Completion.scala @@ -2,7 +2,6 @@ * Copyright 2005-2010 LAMP/EPFL * @author Paul Phillips */ -// $Id$ // // TODO, if practical: @@ -11,345 +10,102 @@ // Possible approach: evaluate buffer as if current identifier is // 2) Implicits: x.<tab> should show not only x's members but those of anything for which // there is an implicit conversion from x. -// 3) Chaining: x.foo(bar).<tab> should complete on foo's result type. // 4) Imports: after import scala.collection.mutable._, HashMap should be among // my top level identifiers. -// 5) Caching: it's silly to parse all the jars on every startup, we should have -// a peristent store somewhere we can write and only check last-mod dates. -// 6) Security: Are we using the filesystem unnecessarily? -// +// 5) Caching: parsing the jars every startup seems wasteful, but experimentally +// there is little to no gain from caching. package scala.tools.nsc package interpreter import jline._ import java.net.URL -import java.lang.reflect import java.util.{ List => JList } -import java.util.concurrent.ConcurrentHashMap -import scala.concurrent.DelayedLazyVal -import scala.collection.mutable.HashSet -import scala.util.NameTransformer.{ decode, encode } - -// REPL completor - queries supplied interpreter for valid completions -// based on current contents of buffer. -class Completion( - val interpreter: Interpreter, - val intLoop: InterpreterLoop) -extends Completor { - def this(interpreter: Interpreter) = this(interpreter, null) - import Completion._ - import interpreter.compilerClasspath - - // it takes a little while to look through the jars so we use a future and a concurrent map - class CompletionAgent { - val dottedPaths = new ConcurrentHashMap[String, List[CompletionInfo]] - val topLevelPackages = new DelayedLazyVal( - () => enumToList(dottedPaths.keys) filterNot (_ contains '.'), - getDottedPaths(dottedPaths, interpreter) - ) +// REPL completor - queries supplied interpreter for valid +// completions based on current contents of buffer. +class Completion(repl: Interpreter) extends Completor { + private def asURLs(xs: List[String]) = xs map (x => io.File(x).toURL) + private def classPath = ( + // compiler jars, scala-library.jar etc. + (repl.compilerClasspath) ::: + // boot classpath, java.lang.* etc. + (asURLs(repl.settings.bootclasspath.value split ':' toList)) + ) + + // the unqualified vals/defs/etc visible in the repl + val ids = new IdentCompletion(repl) + // the top level packages we know about + val pkgs = new PackageCompletion(classPath) + + // TODO - restore top level availability of scala.* java.lang.* scala.Predef + // Old code: + // + // def membersOfPredef() = membersOfId("scala.Predef") + // + // def javaLangToHide(s: String) = ( + // (s endsWith "Exception") || + // (s endsWith "Error") || + // (s endsWith "Impl") || + // (s startsWith "CharacterData") || + // !existsAndPublic("java.lang." + s) + // ) + // + // def scalaToHide(s: String) = + // (List("Tuple", "Product", "Function") exists (x => (x + """\d+""").r findPrefixMatchOf s isDefined)) || + // (List("Exception", "Error") exists (s endsWith _)) + // + // import reflect.Modifier.{ isPrivate, isProtected, isPublic, isStatic } + // private def isSingleton(x: Int, isJava: Boolean) = !isJava || isStatic(x) + // private def existsAndPublic(s: String): Boolean = + // (dottedPaths containsKey s) || { + // val clazz = + // try Class.forName(s) + // catch { case _: ClassNotFoundException | _: SecurityException => return false } + // + // isPublic(clazz.getModifiers) + // } + + // the high level analysis + def analyze(_buffer: String, clist: JList[String]): Int = { + val parsed = new Parsed(_buffer) + import parsed._ + + val candidates = List(ids, pkgs) flatMap (_ completionsFor buffer) + candidates foreach (clist add _) + position } - val agent = new CompletionAgent - import agent._ - - import reflect.Modifier.{ isPrivate, isProtected, isPublic, isStatic } - private def isSingleton(x: Int, isJava: Boolean) = !isJava || isStatic(x) - private def existsAndPublic(s: String): Boolean = - (dottedPaths containsKey s) || { - val clazz = - try Class.forName(s) - catch { case _: ClassNotFoundException | _: SecurityException => return false } - - isPublic(clazz.getModifiers) - } - - // One instance of a command line - class Buffer(s: String, verbose: Boolean) { - val buffer = if (s == null) "" else s - def isEmptyBuffer = buffer == "" - - val segments = buffer.split("\\.", -1).toList - val lastDot = buffer.lastIndexOf('.') - val hasDot = segments.size > 0 && segments.last == "" - - // given foo.bar.baz, path = foo.bar and stub = baz - val (path, stub) = segments.size match { - case 0 => ("", "") - case 1 => (segments.head, "") - case _ => (segments.init.mkString("."), segments.last) - } - - def filt(xs: List[String]) = xs filter (_ startsWith stub) - - case class Result(candidates: List[String], position: Int) { - def getCandidates() = (candidates map (_.trim) removeDuplicates) sortWith (_ < _) - } - - // work out completion candidates and position - def analyzeBuffer(clist: JList[String]): Result = { - lazy val ids = idsStartingWith(path) - lazy val pkgs = pkgsStartingWith(path) - lazy val count = (ids ::: pkgs).size - - def doSimple(): Result = count match { - case 0 => Result(Nil, 0) - case 1 if pkgs.size > 0 => Result(pkgs, 0) - case 1 if buffer.length < ids.head.length => Result(ids, 0) - case 1 => Result(ids, 0) - // XXX for now commented out "dot inference" because it's overcomplicated - // val members = membersOfId(ids.head) filter (_ startsWith stub) - // if (members.isEmpty) Result(Nil, 0) - // else Result(members, path.length + 1) - case _ => Result(ids ::: pkgs, 0) - } - - def doDotted(): Result = { - def pkgs = membersOfPath(path) - def ids = membersOfId(path) - def idExtras = List("isInstanceOf", "asInstanceOf", "toString") - def statics = completeStaticMembers(path) - def pkgMembers = completePackageMembers(path) - - def calcList = if (pkgs.isEmpty) ids ::: idExtras ::: statics else pkgs - def idList = filt(calcList ::: pkgMembers) - - Result(idList.removeDuplicates, path.length + 1) - } - segments.size match { - case 0 => Result(Nil, 0) - case 1 => doSimple() - case _ => doDotted() - } - } + // chasing down results which won't parse + def execute(line: String): Option[Any] = { + val parsed = new Parsed(line) + import parsed._ - def isValidId(s: String) = interpreter.unqualifiedIds contains s - def membersOfId(s: String) = interpreter membersOfIdentifier s - def membersOfPath(s: String) = { - val xs = - if (dottedPaths containsKey s) dottedPaths get s map (_.visibleName) - else Nil + if (!isQualified) + return None - s match { - case "scala" => xs filterNot scalaToHide - case "java.lang" => xs filterNot javaLangToHide - case _ => xs - } - } - def membersOfPredef() = membersOfId("scala.Predef") + for (topLevel <- List(ids, pkgs) ; exec <- topLevel executionFor buffer) + return Some(exec) - def javaLangToHide(s: String) = ( - (s endsWith "Exception") || - (s endsWith "Error") || - (s endsWith "Impl") || - (s startsWith "CharacterData") || - !existsAndPublic("java.lang." + s) - ) - - def scalaToHide(s: String) = - (List("Tuple", "Product", "Function") exists (x => (x + """\d+""").r findPrefixMatchOf s isDefined)) || - (List("Exception", "Error") exists (s endsWith _)) - - /** Hide all default members not verbose */ - def defaultMembers = - if (verbose) (List("scala", "java.lang") flatMap membersOfPath) ::: membersOfPredef - else Nil - - def pkgsStartingWith(s: String) = topLevelPackages() filter (_ startsWith s) - def idsStartingWith(s: String) = { - // only print res* when verbose - val unqIds = - if (verbose) interpreter.unqualifiedIds - else interpreter.unqualifiedIds filterNot (_ startsWith INTERPRETER_VAR_PREFIX) - - (unqIds ::: defaultMembers) filter (_ startsWith s) - } - - def complete(clist: JList[String]): Int = { - val res = analyzeBuffer(clist) - res.getCandidates foreach (x => clist add decode(x)) - res.position - } + None } - private def getMembers(c: Class[_], isJava: Boolean): List[String] = - c.getMethods.toList . - filter (x => isPublic(x.getModifiers)) . - filter (x => isSingleton(x.getModifiers, isJava)) . - map (_.getName) . - filterNot (shouldHide) - - private def getClassObject(path: String): Option[Class[_]] = - (interpreter getClassObject path) orElse - (interpreter getClassObject ("scala." + path)) orElse - (interpreter getClassObject ("java.lang." + path)) - - def lastHistoryItem = Option(intLoop) map (_.historyList.last) + // override if history is available + def lastCommand: Option[String] = None // For recording the buffer on the last tab hit private var lastTab: (String, String) = (null, null) // Does this represent two consecutive tabs? - def isConsecutiveTabs(buf: String) = (buf, lastHistoryItem orNull) == lastTab + def isConsecutiveTabs(buf: String) = (buf, lastCommand orNull) == lastTab - // jline's completion comes through here - we ask a Buffer for the candidates. + // This is jline's entry point for completion. override def complete(_buffer: String, cursor: Int, candidates: JList[String]): Int = { // println("_buffer = %s, cursor = %d".format(_buffer, cursor)) val verbose = isConsecutiveTabs(_buffer) - lastTab = (_buffer, lastHistoryItem orNull) - - new Buffer(_buffer, verbose) complete candidates - } - - def completePackageMembers(path: String): List[String] = - getClassObject(path + "." + "package") map (getMembers(_, false)) getOrElse Nil - - def completeStaticMembers(path: String): List[String] = { - // java style, static methods - val js = getClassObject(path) map (getMembers(_, true)) getOrElse Nil - // scala style, methods on companion object - // if getClassObject fails, see if there is a type alias - val clazz = getClassObject(path + "$") orElse { - (ByteCode aliasForType path) flatMap (x => getClassObject(x + "$")) - } - val ss = clazz map (getMembers(_, false)) getOrElse Nil - - js ::: ss - } -} - -object Completion -{ - import java.io.File - import java.util.jar.{ JarEntry, JarFile } - import scala.tools.nsc.io.Streamable - - val EXPAND_SEPARATOR_STRING = "$$" - val ANON_CLASS_NAME = "$anon" - val TRAIT_SETTER_SEPARATOR_STRING = "$_setter_$" - val IMPL_CLASS_SUFFIX ="$class" - val INTERPRETER_VAR_PREFIX = "res" - - /** Interface for objects which supply their own completion contents. - */ - trait Special { - def tabCompletions(): List[String] - } - - case class CompletionInfo(visibleName: String, className: String, jar: String) { - lazy val jarfile = new JarFile(jar) - lazy val entry = jarfile getEntry className - - override def hashCode = visibleName.hashCode - override def equals(other: Any) = other match { - case x: CompletionInfo => visibleName == x.visibleName - case _ => false - } - - def getBytes(): Array[Byte] = { - if (entry == null) Array() else { - val x = new Streamable.Bytes { def inputStream() = jarfile getInputStream entry } - x.toByteArray() - } - } - } - - def enumToList[T](e: java.util.Enumeration[T]): List[T] = enumToList(e, Nil) - def enumToList[T](e: java.util.Enumeration[T], xs: List[T]): List[T] = - if (e == null || !e.hasMoreElements) xs else enumToList(e, e.nextElement :: xs) - - // methods to leave out of completion - val excludeMethods = List("", "hashCode", "equals", "wait", "notify", "notifyAll") - - private def exists(path: String) = new File(path) exists - - def shouldHide(x: String) = - (excludeMethods contains x) || - (x contains ANON_CLASS_NAME) || - (x contains TRAIT_SETTER_SEPARATOR_STRING) || - (x endsWith IMPL_CLASS_SUFFIX) - - def getClassFiles(path: String): List[String] = { - if (!exists(path)) return Nil - - (enumToList(new JarFile(path).entries) map (_.getName)) . - partialMap { case x: String if x endsWith ".class" => x dropRight 6 } . - filterNot { shouldHide } - } - - // all the dotted path to classfiles we can find by poking through the jars - def getDottedPaths( - map: ConcurrentHashMap[String, List[CompletionInfo]], - interpreter: Interpreter): Unit = - { - val cp = - interpreter.compilerClasspath.map(_.getPath) ::: // compiler jars, scala-library.jar etc. - interpreter.settings.bootclasspath.value.split(':').toList // boot classpath, java.lang.* etc. - - val jars = cp.removeDuplicates filter (_ endsWith ".jar") - - // for e.g. foo.bar.baz.C, returns (foo -> bar), (foo.bar -> baz), (foo.bar.baz -> C) - // and scala.Range$BigInt needs to go scala -> Range -> BigInt - def subpaths(s: String): List[(String, String)] = { - val segs = decode(s).split("""[/.]""") - val components = segs dropRight 1 - - (1 to components.length).toList flatMap { i => - val k = components take i mkString "." - if (segs(i) contains "$") { - val dollarsegs = segs(i).split("$").toList - for (j <- 1 to (dollarsegs.length - 1) toList) yield { - val newk = k + "." + (dollarsegs take j mkString ".") - (k -> dollarsegs(j)) - } - } - else List(k -> segs(i)) - } - } - - def oneJar(jar: String): Unit = { - val classfiles = Completion getClassFiles jar - - for (cl <- classfiles.removeDuplicates ; (k, _v) <- subpaths(cl)) { - val v = CompletionInfo(_v, cl, jar) - - if (map containsKey k) { - val vs = map.get(k) - if (vs contains v) () - else map.put(k, v :: vs) - } - else map.put(k, List(v)) - } - } - - jars foreach oneJar - } - - /** The methods below this point exist to simplify repl-generated code. - */ - - // XXX at the moment this is imperfect because scala's protected semantics - // differ from java's, so protected methods appear public via reflection; - // yet scala enforces the protection. The result is that protected members - // appear in completion yet cannot actually be called. Fixing this - // properly requires a scala.reflect.* API. Fixing it uglily is possible - // too (cast to structural type!) but I deem poor use of energy. - private def skipModifiers(m: reflect.Method) = { - import java.lang.reflect.Modifier._ - val flags = STATIC | PRIVATE | PROTECTED - (m.getModifiers & flags) == 0 - } - private def getAnyClass(x: Any): Class[_] = x.asInstanceOf[AnyRef].getClass - - def methodsOf(target: Any): List[String] = - getAnyClass(target).getMethods filter skipModifiers map (_.getName) toList - - // getAnyClass(target).getInterfaces exists (_ == specialClazz) - // private val specialClazz = classOf[Special] + lastTab = (_buffer, lastCommand orNull) - def selfDefinedMembers(target: Any) = target match { - case x: Special => Some(x.tabCompletions()) - case _ => None + // modify the buffer in place and returns the cursor position + analyze(_buffer, candidates) } } diff --git a/src/compiler/scala/tools/nsc/interpreter/CompletionAware.scala b/src/compiler/scala/tools/nsc/interpreter/CompletionAware.scala new file mode 100644 index 0000000000..c60f402d3d --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/CompletionAware.scala @@ -0,0 +1,84 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import scala.util.NameTransformer + +/** An interface for objects which are aware of tab completion and + * will supply their own candidates and resolve their own paths. + */ +trait CompletionAware { + /** The complete list of unqualified Strings to which this + * object will complete. + */ + def completions(): List[String] + def completions(start: String): List[String] = completions filter (_ startsWith start) + + /** Default filter to apply to completions. + */ + def filterNotFunction(s: String): Boolean = ReflectionCompletion shouldHide s + + /** Default sort. + */ + def sortFunction(s1: String, s2: String): Boolean = s1 < s2 + + /** Default map. + */ + def mapFunction(s: String): String = s + + /** The next completor in the chain. + */ + def follow(id: String): Option[CompletionAware] = None + + /** What to return if this completion is given as a command. It + * returns None by default, which means to allow the repl to interpret + * the line normally. Returning Some(_) means the line will never + * reach the scala interpreter. + */ + def execute(id: String): Option[Any] = None + + /** Given string 'buf', return a list of all the strings + * to which it can complete. This may involve delegating + * to other CompletionAware objects. + */ + def completionsFor(buf: String): List[String] = { + val parsed = new Parsed(buf) + import parsed._ + + ( + if (isEmpty) completions() + else if (isFirstCharDot) Nil // XXX for now + else if (isUnqualified && !isLastCharDot) completions(buf) + else follow(hd) match { + case Some(next) => next completionsFor remainder + case _ => Nil + } + ) filterNot filterNotFunction map mapFunction sortWith (sortFunction _) + } + + def executionFor(buf: String): Option[Any] = { + val parsed = new Parsed(buf) + import parsed._ + + if (isUnqualified && !isLastCharDot && (completions contains buf)) execute(buf) + else if (!isQualified) None + else follow(hd) match { + case Some(next) => next executionFor remainder + case _ => None + } + } +} + +object CompletionAware { + def unapply(that: Any): Option[CompletionAware] = that match { + case x: CompletionAware => Some((x)) + case _ => None + } + + def apply(xs: List[String]) = new CompletionAware { val completions = xs } +} + diff --git a/src/compiler/scala/tools/nsc/interpreter/History.scala b/src/compiler/scala/tools/nsc/interpreter/History.scala index 4f726ebc55..519d17f9d2 100644 --- a/src/compiler/scala/tools/nsc/interpreter/History.scala +++ b/src/compiler/scala/tools/nsc/interpreter/History.scala @@ -6,12 +6,31 @@ package scala.tools.nsc package interpreter +import java.io.File +import jline.{ ConsoleReader, History => JHistory } import scala.collection.JavaConversions.asBuffer /** Primarily, a wrapper for JLine's History. */ -class History(jhistory: jline.History) { +class History(val jhistory: JHistory) { def asJavaList = jhistory.getHistoryList def asList: List[String] = asBuffer(asJavaList).toList def index = jhistory.getCurrentIndex -}
\ No newline at end of file + + def grep(s: String) = asList filter (_ contains s) +} + +object History { + val ScalaHistoryFile = ".scala_history" + def homeDir = System.getProperty("user.home") + + def apply(reader: ConsoleReader): History = + if (reader == null) apply() + else new History(reader.getHistory) + + def apply(): History = new History( + try new JHistory(new File(homeDir, ScalaHistoryFile)) + // do not store history if error + catch { case _: Exception => new JHistory() } + ) +} diff --git a/src/compiler/scala/tools/nsc/interpreter/IdentCompletion.scala b/src/compiler/scala/tools/nsc/interpreter/IdentCompletion.scala new file mode 100644 index 0000000000..5759abdb97 --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/IdentCompletion.scala @@ -0,0 +1,19 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +/** Top level identifiers visible in the repl. It immediately + * delegates to an InstanceCompletion. + */ +class IdentCompletion(repl: Interpreter) extends CompletionAware { + def completions() = repl.unqualifiedIds + override def follow(id: String) = + if (completions contains id) + repl completionAware id orElse Some(new InstanceCompletion(repl clazzForIdent id)) + else + None +} diff --git a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala index 7f89669806..932450b01a 100644 --- a/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/InteractiveReader.scala @@ -25,6 +25,10 @@ trait InteractiveReader { // overide if history is available def history: Option[History] = None + def historyList = history map (_.asList) getOrElse Nil + + // override if completion is available + def completion: Option[Completion] = None // hack necessary for OSX jvm suspension because read calls are not restarted after SIGTSTP private def restartSystemCall(e: Exception): Boolean = @@ -41,9 +45,12 @@ object InteractiveReader { /** Create an interactive reader. Uses <code>JLineReader</code> if the * library is available, but otherwise uses a <code>SimpleReader</code>. */ - def createDefault(interpreter: Interpreter, intLoop: InterpreterLoop = null): InteractiveReader = - catching(exes: _*) - . opt (new JLineReader(interpreter, intLoop)) - . getOrElse (new SimpleReader) + def createDefault(interpreter: Interpreter): InteractiveReader = + try new JLineReader(interpreter) + catch { + case e @ (_: Exception | _: NoClassDefFoundError) => + // println("Failed to create JLineReader(%s): %s".format(interpreter, e)) + new SimpleReader + } } diff --git a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala index c0fe874a37..d9ce962ca3 100644 --- a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala +++ b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala @@ -11,28 +11,27 @@ import java.io.File import jline.{ ConsoleReader, ArgumentCompletor, History => JHistory } /** Reads from the console using JLine */ -class JLineReader(interpreter: Interpreter, intLoop: InterpreterLoop) extends InteractiveReader { - def this() = this(null, null) - def this(interpreter: Interpreter) = this(interpreter, null) - override def history = Some(new History(consoleReader.getHistory)) +class JLineReader(interpreter: Interpreter) extends InteractiveReader { + def this() = this(null) - val consoleReader = { - val history = - try new JHistory(new File(System.getProperty("user.home"), ".scala_history")) - // do not store history if error - catch { case _: Exception => new JHistory() } + override lazy val history = Some(History(consoleReader)) + override lazy val completion = + if (interpreter == null) None + else Some(new Completion(interpreter)) + val consoleReader = { val r = new jline.ConsoleReader() - r setHistory history + r setHistory (History().jhistory) r setBellEnabled false if (interpreter != null) { // have to specify all delimiters for completion to work nicely val delims = new ArgumentCompletor.AbstractArgumentDelimiter { - val delimChars = "(){}[],`;'\" \t".toArray + // val delimChars = "(){}[],`;'\" \t".toArray + val delimChars = "(){},`; \t".toArray def isDelimiterChar(s: String, pos: Int) = delimChars contains s.charAt(pos) } - val comp = new ArgumentCompletor(new Completion(interpreter, intLoop), delims) + val comp = new ArgumentCompletor(completion.get, delims) comp setStrict false r addCompletor comp // XXX make this use a setting diff --git a/src/compiler/scala/tools/nsc/interpreter/PackageCompletion.scala b/src/compiler/scala/tools/nsc/interpreter/PackageCompletion.scala new file mode 100644 index 0000000000..f1c6717b2b --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/PackageCompletion.scala @@ -0,0 +1,129 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import java.net.URL +import java.lang.reflect +import java.util.concurrent.ConcurrentHashMap +import scala.concurrent.DelayedLazyVal +import scala.util.NameTransformer.{ decode, encode } +import PackageCompletion._ + +/** Completion among all known packages. It examines the jars in a + * separate thread so as not to slow down startup. If it arrives at + * an object, it delegates to StaticCompletion for that object. + */ +class PackageCompletion(classpath: List[URL]) extends CompletionAware { + // it takes a little while to look through the jars so we use a future and a concurrent map + class CompletionAgent { + val dottedPaths: ConcurrentHashMap[String, List[CompletionInfo]] = new ConcurrentHashMap[String, List[CompletionInfo]] + val topLevelPackages = new DelayedLazyVal( + () => enumToList(dottedPaths.keys) filterNot (_ contains '.'), + getDottedPaths(dottedPaths, classpath) + ) + } + val agent = new CompletionAgent + import agent._ + + def completions() = topLevelPackages() + override def follow(id: String) = + if (dottedPaths containsKey id) Some(new SubCompletor(id)) + else None + + class SubCompletor(root: String) extends CompletionAware { + private def infos = dottedPaths get root + def completions() = infos map (_.visibleName) + + override def follow(segment: String): Option[CompletionAware] = { + PackageCompletion.this.follow(root + "." + segment) orElse { + for (CompletionInfo(`segment`, className, _) <- infos) { + return Some(new StaticCompletion(className)) + } + None + } + } + } +} + +object PackageCompletion { + import java.io.File + import java.util.jar.{ JarEntry, JarFile } + import scala.tools.nsc.io.Streamable + + def enumToList[T](e: java.util.Enumeration[T]): List[T] = enumToListInternal(e, Nil) + private def enumToListInternal[T](e: java.util.Enumeration[T], xs: List[T]): List[T] = + if (e == null || !e.hasMoreElements) xs else enumToListInternal(e, e.nextElement :: xs) + + def getClassFiles(path: String): List[String] = { + def exists(path: String) = { new File(path) exists } + if (!exists(path)) return Nil + + (enumToList(new JarFile(path).entries) map (_.getName)) . + partialMap { case x: String if x endsWith ".class" => x dropRight 6 } . + filterNot { ReflectionCompletion.shouldHide } + } + + case class CompletionInfo(visibleName: String, className: String, jar: String) { + lazy val jarfile = new JarFile(jar) + lazy val entry = jarfile getEntry className + + override def hashCode = visibleName.hashCode + override def equals(other: Any) = other match { + case x: CompletionInfo => visibleName == x.visibleName + case _ => false + } + + def getBytes(): Array[Byte] = { + if (entry == null) Array() else { + val x = new Streamable.Bytes { def inputStream() = jarfile getInputStream entry } + x.toByteArray() + } + } + } + + // all the dotted path to classfiles we can find by poking through the jars + def getDottedPaths(map: ConcurrentHashMap[String, List[CompletionInfo]], classpath: List[URL]): Unit = { + val cp = classpath map (_.getPath) + val jars = cp.removeDuplicates filter (_ endsWith ".jar") + + // for e.g. foo.bar.baz.C, returns (foo -> bar), (foo.bar -> baz), (foo.bar.baz -> C) + // and scala.Range$BigInt needs to go scala -> Range -> BigInt + def subpaths(s: String): List[(String, String)] = { + val segs = decode(s).split("""[/.]""") + val components = segs dropRight 1 + + (1 to components.length).toList flatMap { i => + val k = components take i mkString "." + if (segs(i) contains "$") { + val dollarsegs = segs(i).split("$").toList + for (j <- 1 to (dollarsegs.length - 1) toList) yield { + val newk = k + "." + (dollarsegs take j mkString ".") + (k -> dollarsegs(j)) + } + } + else List(k -> segs(i)) + } + } + + def oneJar(jar: String): Unit = { + val classfiles = getClassFiles(jar) + + for (cl <- classfiles.removeDuplicates ; (k, _v) <- subpaths(cl)) { + val v = CompletionInfo(_v, cl, jar) + + if (map containsKey k) { + val vs = map.get(k) + if (vs contains v) () + else map.put(k, v :: vs) + } + else map.put(k, List(v)) + } + } + + jars foreach oneJar + } +}
\ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/interpreter/Parsed.scala b/src/compiler/scala/tools/nsc/interpreter/Parsed.scala new file mode 100644 index 0000000000..0c0104b8fa --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/Parsed.scala @@ -0,0 +1,30 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +/** One instance of a command buffer. + */ +class Parsed(_buf: String) { + val buffer = if (_buf == null) "" else _buf + val segments = (buffer split '.').toList filterNot (_ == "") + lazy val hd :: tl = segments + def stub = firstDot + hd + "." + def remainder = buffer stripPrefix stub + + def isEmpty = segments.size == 0 + def isUnqualified = segments.size == 1 + def isQualified = segments.size > 1 + + def isFirstCharDot = buffer startsWith "." + def isLastCharDot = buffer endsWith "." + def firstDot = if (isFirstCharDot) "." else "" + def lastDot = if (isLastCharDot) "." else "" + + // sneakily, that is 0 when there is no dot, which is what we want + def position = (buffer lastIndexOf '.') + 1 +} + diff --git a/src/compiler/scala/tools/nsc/interpreter/ReflectionCompletion.scala b/src/compiler/scala/tools/nsc/interpreter/ReflectionCompletion.scala new file mode 100644 index 0000000000..e65635dc1d --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/ReflectionCompletion.scala @@ -0,0 +1,139 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import java.lang.reflect +import reflect.Modifier.{ isPrivate, isProtected, isPublic, isStatic } +import scala.util.NameTransformer +import scala.collection.mutable.HashMap +import ReflectionCompletion._ + +/** A completion aware object representing a single instance of some class. + * It completes to instance fields and methods, and delegates to another + * InstanceCompletion object if it can determine the result type of the element. + */ +class InstanceCompletion(clazz: Class[_]) extends CompletionAware { + def methods = clazz.getMethods.toList filterNot (x => isStatic(x.getModifiers)) + def fields = clazz.getFields.toList filterNot (x => isStatic(x.getModifiers)) + val (zeroArg, otherArg) = methods partition (_.getParameterTypes.size == 0) + + lazy val completions = (methods ::: fields) map (_.getName) + override def mapFunction(s: String) = NameTransformer decode s + + // TODO + // def idExtras = List("isInstanceOf", "asInstanceOf", "toString") + + override def follow(id: String) = { + val nextClazz = zeroArg find (m => m.getName == id) map (_.getReturnType) + if (nextClazz.isDefined) nextClazz map (x => new InstanceCompletion(x)) + else fields find (_.getName == id) map (x => new InstanceCompletion(x.getType)) + } +} + +/** The complementary class to InstanceCompletion. It has logic to deal with + * java static members and scala companion object members. + */ +class StaticCompletion(jarEntryName: String) extends CompletionAware { + def className = jarEntryName.replace('/', '.') + def isScalaClazz(cl: Class[_]) = allInterfaces(cl) exists (_.getName == "scala.ScalaObject") + def isJava = !isScalaClazz(clazz) + + lazy val clazz = { + val cl = Class.forName(className) + if (className.last != '$' && isScalaClazz(cl)) { + try Class.forName(className + "$") + catch { case _: Exception => cl } + } + else cl + } + + def methodFilter: reflect.Method => Boolean = + if (isJava) m => isStatic(m.getModifiers) && isPublic(m.getModifiers) + else m => isPublic(m.getModifiers) + + def methods = clazz.getMethods.toList filter methodFilter + def fields = clazz.getFields.toList + + lazy val completions = (methods ::: fields) map (_.getName) + override def mapFunction(s: String) = NameTransformer decode s + + // TODO - old version. + // + // private def getClassObject(path: String): Option[Class[_]] = { + // val cl = clazz.getClassLoader() + // try Some(Class.forName(path, true, cl).asInstanceOf[Class[_]]) + // catch { case _ => None } + // } + // + // def completeStaticMembers(path: String): List[String] = { + // // java style, static methods + // val js = getClassObject(path) map (getMembers(_, true)) getOrElse Nil + // // scala style, methods on companion object + // // if getClassObject fails, see if there is a type alias + // val clazz = getClassObject(path + "$") orElse { + // (ByteCode aliasForType path) flatMap (x => getClassObject(x + "$")) + // } + // val ss = clazz map (getMembers(_, false)) getOrElse Nil + // + // js ::: ss + // } +} + +// TODO +class PackageObjectCompletion(packageName: String) extends CompletionAware { + def completions() = error("TODO") + + // def completePackageMembers(path: String): List[String] = + // getClassObject(path + "." + "package") map (getMembers(_, false)) getOrElse Nil +} + +class ReflectionCompletion { } +object ReflectionCompletion { + import java.io.File + import java.util.jar.{ JarEntry, JarFile } + import scala.tools.nsc.io.Streamable + + val EXPAND_SEPARATOR_STRING = "$$" + val ANON_CLASS_NAME = "$anon" + val TRAIT_SETTER_SEPARATOR_STRING = "$_setter_$" + val IMPL_CLASS_SUFFIX ="$class" + val INTERPRETER_VAR_PREFIX = "res" + + def allInterfaces(clazz: Class[_]): List[Class[_]] = allInterfaces(clazz, Nil) + def allInterfaces(clazz: Class[_], acc: List[Class[_]]): List[Class[_]] = { + if (clazz == null) acc.removeDuplicates + else allInterfaces(clazz.getSuperclass, acc ::: clazz.getInterfaces.toList) + } + + // methods to leave out of completion + val excludeMethods = List("", "hashCode", "equals", "wait", "notify", "notifyAll") + + def shouldHide(x: String) = + (excludeMethods contains x) || + (x contains EXPAND_SEPARATOR_STRING) || // XXX + (x contains ANON_CLASS_NAME) || + (x contains TRAIT_SETTER_SEPARATOR_STRING) || + (x endsWith IMPL_CLASS_SUFFIX) || + (x == "MODULE$") || + (x matches """.*\$\d+$""") + + // XXX at the moment this is imperfect because scala's protected semantics + // differ from java's, so protected methods appear public via reflection; + // yet scala enforces the protection. The result is that protected members + // appear in completion yet cannot actually be called. Fixing this + // properly requires a scala.reflect.* API. Fixing it uglily is possible + // too (cast to structural type!) but I deem poor use of energy. + private def skipModifiers(m: reflect.Method) = { + import java.lang.reflect.Modifier._ + val flags = STATIC | PRIVATE | PROTECTED + (m.getModifiers & flags) == 0 + } + private def getAnyClass(x: Any): Class[_] = x.asInstanceOf[AnyRef].getClass + + def methodsOf(target: Any): List[String] = + getAnyClass(target).getMethods filter skipModifiers map (_.getName) toList +} diff --git a/src/compiler/scala/tools/nsc/interpreter/XMLCompletion.scala b/src/compiler/scala/tools/nsc/interpreter/XMLCompletion.scala new file mode 100644 index 0000000000..18bc136ce7 --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/XMLCompletion.scala @@ -0,0 +1,43 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import xml.{ XML, Group, Node, NodeSeq } +import XMLCompletion._ +import scala.collection.mutable.HashMap + +class XMLCompletion(root: Node) extends CompletionAware { + private val nodeCache = new HashMap[String, Node] + private def getNode(s: String): Option[Node] = { + completions // make sure cache is populated + nodeCache get s + } + + lazy val completions: List[String] = { + def children = root.child.toList + def uniqueTags = children groupBy (_.label) filter (_._2.size == 1) map (_._1) + val uniqs = uniqueTags.toList + + children.foldLeft(List[String]())((res, node) => { + val name = node.label + def count = res filter (_ startsWith (name + "[")) size // ] + val suffix = if (uniqs contains name) "" else "[%d]" format (count + 1) + val s = name + suffix + + nodeCache(s) = node + + s :: res + }).sortWith (_ < _) + } + + override def execute(id: String) = getNode(id) + override def follow(id: String) = getNode(id) map (x => new XMLCompletion(x)) +} + +object XMLCompletion { + def apply(x: Node) = new XMLCompletion(x) +} diff --git a/src/library/scala/collection/Iterator.scala b/src/library/scala/collection/Iterator.scala index 32525d59a6..7dbc39f5b7 100644 --- a/src/library/scala/collection/Iterator.scala +++ b/src/library/scala/collection/Iterator.scala @@ -1121,6 +1121,17 @@ trait Iterator[+A] { self => res.toList } + /** Traverses this iterator and returns all produced values in a set. + * $willNotTerminateInf + * + * @return a set which contains all values produced by this iterator. + */ + def toSet[B >: A]: immutable.Set[B] = { + val res = new ListBuffer[B] + while (hasNext) res += next + res.toSet + } + /** Lazily wraps a Stream around this iterator so its values are memoized. * * @return a Stream which can repeatedly produce all the values @@ -1140,6 +1151,20 @@ trait Iterator[+A] { self => buffer } + /** Traverses this iterator and returns all produced values in a map. + * $willNotTerminateInf + * @see TraversableLike.toMap + * + * @return a map containing all elements of this iterator. + */ + def toMap[T, U](implicit ev: A <:< (T, U)): immutable.Map[T, U] = { + val b = immutable.Map.newBuilder[T, U] + while (hasNext) + b += next + + b.result + } + /** Tests if another iterator produces the same valeus as this one. * $willNotTerminateInf * @param that the other iterator diff --git a/src/library/scala/xml/NodeSeq.scala b/src/library/scala/xml/NodeSeq.scala index 17ea9228f6..02c1b6ece8 100644 --- a/src/library/scala/xml/NodeSeq.scala +++ b/src/library/scala/xml/NodeSeq.scala @@ -80,8 +80,8 @@ abstract class NodeSeq extends immutable.Seq[Node] with SeqLike[Node, NodeSeq] { * @return ... */ def \(that: String): NodeSeq = { + def fail = throw new IllegalArgumentException(that) def atResult = { - def fail = throw new IllegalArgumentException(that) lazy val y = this(0) val attr = if (that.length == 1) fail @@ -104,6 +104,7 @@ abstract class NodeSeq extends immutable.Seq[Node] with SeqLike[Node, NodeSeq] { NodeSeq fromSeq (this flatMap (_.child) filter cond) that match { + case "" => fail case "_" => makeSeq(!_.isAtom) case _ if (that(0) == '@' && this.length == 1) => atResult case _ => makeSeq(_.label == that) |