summaryrefslogtreecommitdiff
path: root/src/compiler
diff options
context:
space:
mode:
authorPaul Phillips <paulp@improving.org>2009-04-08 13:21:43 +0000
committerPaul Phillips <paulp@improving.org>2009-04-08 13:21:43 +0000
commit4e2f93073a79e49321fd06f0a69ac120b448ba0c (patch)
tree755460208d58f135c0ad8c0f678385b8f1f5b79f /src/compiler
parent339cbf16da2fc92f88274b5f7b5183e4647a17f2 (diff)
downloadscala-4e2f93073a79e49321fd06f0a69ac120b448ba0c.tar.gz
scala-4e2f93073a79e49321fd06f0a69ac120b448ba0c.tar.bz2
scala-4e2f93073a79e49321fd06f0a69ac120b448ba0c.zip
Path completion! You can now say...
scala.co<tab> and be offered collection compat concurrent It looks through all the jars on your classpath and the boot classpath for completion options, but does so asynchronously so it doesn't hold anything up. It ignores classpath elements which are directories, so as to avoid accidentally traversing the entire filesystem looking for classes.
Diffstat (limited to 'src/compiler')
-rw-r--r--src/compiler/scala/tools/nsc/Interpreter.scala26
-rw-r--r--src/compiler/scala/tools/nsc/interpreter/Completion.scala187
-rw-r--r--src/compiler/scala/tools/nsc/interpreter/JLineReader.scala2
3 files changed, 187 insertions, 28 deletions
diff --git a/src/compiler/scala/tools/nsc/Interpreter.scala b/src/compiler/scala/tools/nsc/Interpreter.scala
index 4342f96a74..4a8837196d 100644
--- a/src/compiler/scala/tools/nsc/Interpreter.scala
+++ b/src/compiler/scala/tools/nsc/Interpreter.scala
@@ -869,7 +869,9 @@ class Interpreter(val settings: Settings, out: PrintWriter) {
* fields and methods of x via reflection and returns their names to jline.
*/
def membersOfIdentifier(line: String): List[String] = {
- val filterMethods = List("", "hashCode", "equals", "wait", "notify", "notifyAll")
+ import Completion.{ isValidCompletion }
+ import scala.tools.nsc.util.NameTransformer.{ decode, encode } // e.g. $plus$plus => ++
+
val res = beQuietDuring {
for (name <- nameOfIdent(line) ; req <- requestForName(name)) yield {
if (interpret("val " + newInternalVarName() + " = " + name + methodsCode) != IR.Success) Nil
@@ -877,12 +879,13 @@ class Interpreter(val settings: Settings, out: PrintWriter) {
val result = prevRequests.last.resultObjectName
val resultObj = Class.forName(result, true, classLoader)
val valMethod = resultObj.getMethod("result")
+ val str = valMethod.invoke(resultObj).toString
- valMethod.invoke(resultObj).toString .
- split(" ").toList .
- filter(x => !filterMethods.contains(x)) .
- filter(!_.contains("$")) .
- removeDuplicates
+ str.substring(str.indexOf('=') + 1).trim .
+ split(" ").toList .
+ map(decode) .
+ filter(isValidCompletion) .
+ removeDuplicates
}
}
}
@@ -891,11 +894,16 @@ class Interpreter(val settings: Settings, out: PrintWriter) {
}
/** Another entry point for tab-completion, ids in scope */
- def unqualifiedIds(line: String): List[String] =
+ def unqualifiedIds(): List[String] =
allBoundNames .
map(_.toString) .
- filter(!isSynthVarName(_)) .
- filter(_ startsWith line)
+ filter(!isSynthVarName(_))
+
+ /** For static/object method completion */
+ def tryToLoadClass(path: String): Option[Class[_]] = {
+ try { Some(Class.forName(path, false, classLoader)) }
+ catch { case _: ClassNotFoundException | _: NoClassDefFoundError => None }
+ }
// debugging
private var debuggingOutput = false
diff --git a/src/compiler/scala/tools/nsc/interpreter/Completion.scala b/src/compiler/scala/tools/nsc/interpreter/Completion.scala
index cf804f4669..6bfb5cdea1 100644
--- a/src/compiler/scala/tools/nsc/interpreter/Completion.scala
+++ b/src/compiler/scala/tools/nsc/interpreter/Completion.scala
@@ -7,34 +7,183 @@
package scala.tools.nsc.interpreter
import jline._
+import java.net.URL
+import java.util.concurrent.ConcurrentHashMap
// REPL completor - queries supplied interpreter for valid completions
// based on current contents of buffer.
class Completion(val interpreter: Interpreter) extends Completor {
+ import Completion._
import java.util.{ List => JList }
+ import interpreter.compilerClasspath
- override def complete(_buffer: String, cursor: Int, candidates: JList[_]): Int = {
- val buffer = if (_buffer == null) "" else _buffer
- val clist = candidates.asInstanceOf[JList[String]]
+ // it takes a little while to look through the jars so we use a future and a concurrent map
+ val dottedPaths = new ConcurrentHashMap[String, List[String]]
+
+ private var doneExaminingJars = false
+ scala.concurrent.ops.future {
+ getDottedPaths(dottedPaths, interpreter)
+ doneExaminingJars = true
+ }
+
+ // Would like to find a nicer way to do this, but this works for now
+ lazy val topLevelPackagesVal = topLevelPackagesDef
+ def topLevelPackagesDef() = enumToList(dottedPaths.keys) filter (x => !x.contains('.'))
+ def topLevelPackages = if (doneExaminingJars) topLevelPackagesVal else topLevelPackagesDef
+
+ // One instance of a command line
+ class Buffer(s: String) {
+ val buffer = if (s == null) "" else s
+ val segments = buffer.split("\\.", -1).toList
val lastDot = buffer.lastIndexOf('.')
- val (path, stub) =
- if (lastDot < 0) (buffer, "")
- else (buffer.substring(0, lastDot), buffer.substring(lastDot + 1))
-
- def completeMembers(needsDot: Boolean): Int = {
- def withDot(s: String) = if (needsDot) "." + s else s
- val members = interpreter membersOfIdentifier path
- members.filter(_ startsWith stub).foreach(x => clist add withDot(x))
- buffer.length - stub.length
+ 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.sort(_ < _)
+ }
+
+ // 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 = {
+ lazy val pkgs = filt(membersOfPath(path))
+ lazy val ids = filt(membersOfId(path))
+ lazy val statics = filt(completeStaticMembers(path))
+
+ if (!pkgs.isEmpty) Result(pkgs, path.length + 1)
+ else if (!ids.isEmpty) Result(ids, path.length + 1)
+ else Result(statics, path.length + 1)
+ }
+
+ segments.size match {
+ case 0 => Result(Nil, 0)
+ case 1 => doSimple()
+ case _ => doDotted()
+ }
}
- // if there is no dot, complete on unqualified identifiers; otherwise, id's members
- lazy val ids = interpreter unqualifiedIds path
- if (lastDot >= 0) completeMembers(false)
- else ids match {
- case Nil => 0
- case x :: Nil if x == path => completeMembers(true) // only one id matches, insert the dot
- case xs => xs foreach (x => clist add x) ; 0
+ def isValidId(s: String) = interpreter.unqualifiedIds contains s
+ def membersOfId(s: String) = interpreter membersOfIdentifier s
+
+ def isValidPath(s: String) = dottedPaths containsKey s
+ def membersOfPath(s: String) = if (isValidPath(s)) dottedPaths get s else Nil
+
+ def pkgsStartingWith(s: String) = topLevelPackages filter (_ startsWith s)
+ def idsStartingWith(s: String) = interpreter.unqualifiedIds filter (_ startsWith s)
+
+ def complete(clist: JList[String]): Int = {
+ val res = analyzeBuffer(clist)
+ res.getCandidates foreach (clist add _)
+ res.position
}
}
+
+ // jline's completion comes through here - we ask a Buffer for the candidates.
+ override def complete(_buffer: String, cursor: Int, candidates: JList[_]): Int =
+ new Buffer(_buffer).complete(candidates.asInstanceOf[JList[String]])
+
+ def completeStaticMembers(path: String): List[String] = {
+ import java.lang.reflect.Modifier.{ isPrivate, isProtected, isStatic }
+ def isVisible(x: Int) = !isPrivate(x) && !isProtected(x)
+ def isSingleton(x: Int, isJava: Boolean) = !isJava || isStatic(x)
+
+ def getMembers(c: Class[_], isJava: Boolean): List[String] =
+ c.getMethods.toList .
+ filter (x => isVisible(x.getModifiers)) .
+ filter (x => isSingleton(x.getModifiers, isJava)) .
+ map (_.getName) .
+ filter (isValidCompletion)
+
+ // java style, static methods
+ val js = interpreter.tryToLoadClass(path).map(getMembers(_, true)) getOrElse Nil
+ // scala style, methods on companion object
+ val ss = interpreter.tryToLoadClass(path + "$").map(getMembers(_, false)) getOrElse Nil
+
+ js ::: ss
+ }
}
+
+object Completion
+{
+ import java.io.File
+ import java.util.jar.{ JarEntry, JarFile }
+
+ 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 isValidCompletion(x: String) = !(x contains "$") && !(excludeMethods contains x)
+ def isClass(x: String) = x endsWith ".class"
+ def dropClass(x: String) = x.substring(0, x.length - 6) // drop .class
+
+ def getClassFiles(path: String): List[String] = {
+ if (!exists(path)) return Nil
+
+ enumToList(new JarFile(path).entries) .
+ map (_.getName) .
+ filter (isClass) .
+ map (dropClass) .
+ filter (isValidCompletion)
+ }
+
+ // all the dotted path to classfiles we can find by poking through the jars
+ def getDottedPaths(
+ map: ConcurrentHashMap[String, List[String]],
+ 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)
+ def subpaths(s: String): List[(String, String)] = {
+ val segs = s.split('.')
+ for (i <- List.range(0, segs.length - 1)) yield {
+ val k = segs.take(i+1).mkString(".")
+ val v = segs(i+1)
+ (k -> v)
+ }
+ }
+
+ def oneJar(jar: String): Unit = {
+ val classfiles = Completion.getClassFiles(jar).map(_.replace('/', '.'))
+ for (cl <- classfiles; (k, v) <- subpaths(cl)) {
+ if (map containsKey k) map.put(k, v :: map.get(k))
+ else map.put(k, List(v))
+ }
+ }
+
+ jars foreach oneJar
+ }
+} \ No newline at end of file
diff --git a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala
index 0e9d494206..0f9a87518a 100644
--- a/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala
+++ b/src/compiler/scala/tools/nsc/interpreter/JLineReader.scala
@@ -32,6 +32,8 @@ class JLineReader(interpreter: Interpreter) extends InteractiveReader {
val comp = new ArgumentCompletor(new Completion(interpreter), delims)
comp setStrict false
r addCompletor comp
+ // XXX make this use a setting
+ r setAutoprintThreshhold 250
}
r