/* NSC -- new Scala compiler * Copyright 2005-2009 LAMP/EPFL * @author Paul Phillips */ // $Id$ // // TODO, if practical: // // 1) Types: val s: String = x.<tab> should only show members which result in a String. // 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? // package scala.tools.nsc package interpreter import jline._ import java.net.URL import java.util.concurrent.ConcurrentHashMap import scala.concurrent.DelayedLazyVal // 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 // 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[String]] val topLevelPackages = new DelayedLazyVal( () => enumToList(dottedPaths.keys) filterNot (_ contains '.'), getDottedPaths(dottedPaths, interpreter) ) } val agent = new CompletionAgent import agent._ // 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 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) } // a few keywords which don't appear as methods via reflection val memberKeywords = List("isInstanceOf", "asInstanceOf") def doDotted(): Result = { lazy val pkgs = filt(membersOfPath(path)) lazy val ids = filt(membersOfId(path)) lazy val idExtras = filt(memberKeywords) // isInstanceOf and asInstanceOf lazy val statics = filt(completeStaticMembers(path)) if (!pkgs.isEmpty) Result(pkgs, path.length + 1) else if (!ids.isEmpty) Result(ids ::: idExtras, path.length + 1) else Result(statics ::: idExtras, path.length + 1) } segments.size match { case 0 => Result(Nil, 0) case 1 => doSimple() case _ => doDotted() } } 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 // XXX generalize this to look through imports def membersOfScala() = membersOfPath("scala") def membersOfJavaLang() = membersOfPath("java.lang") def membersOfPredef() = membersOfId("scala.Predef") def defaultMembers = { val xs = membersOfScala ::: membersOfJavaLang ::: membersOfPredef val excludes = List("""Tuple\d+""".r, """Product\d+""".r, """Function\d+""".r, """.*Exception$""".r, """.*Error$""".r) xs filter (x => excludes forall (r => r.findFirstMatchIn(x).isEmpty)) } def pkgsStartingWith(s: String) = topLevelPackages() filter (_ startsWith s) def idsStartingWith(s: String) = (interpreter.unqualifiedIds ::: defaultMembers) 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[String]): Int = new Buffer(_buffer).complete(candidates) 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) def getClassObject(path: String): Option[Class[_]] = (interpreter getClassObject path) orElse (interpreter getClassObject ("scala." + path)) orElse (interpreter getClassObject ("java.lang." + path)) // java style, static methods val js = getClassObject(path) map (getMembers(_, true)) getOrElse Nil // scala style, methods on companion object val ss = getClassObject(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) // and scala.Range$BigInt needs to go scala -> Range -> BigInt 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 = { def cleanup(s: String): String = s map { c => if (c == '/' || c == '$') '.' else c } toString val classfiles = Completion getClassFiles jar map cleanup 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 } }