summaryrefslogtreecommitdiff
path: root/src/compiler/scala/tools/nsc/interpreter/Completion.scala
blob: bbda31e2e8ced84780be10d0a3aa0c7b4e80da07 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
/* 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.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

  // 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 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

    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 getClassObject path).map(getMembers(_, true)) getOrElse Nil
    // scala style, methods on companion object
    val ss = (interpreter 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
  }
}