diff options
author | Paul Phillips <paulp@improving.org> | 2010-02-01 22:35:12 +0000 |
---|---|---|
committer | Paul Phillips <paulp@improving.org> | 2010-02-01 22:35:12 +0000 |
commit | b80125cb3fa7ab12b5921ee930c04e9d95384861 (patch) | |
tree | 2e3c09c8c62a7e13facef213d01e0d72009e432b | |
parent | 3282ac260cebe12a2d0dcb6bb7c0e479e0e20c6c (diff) | |
download | scala-b80125cb3fa7ab12b5921ee930c04e9d95384861.tar.gz scala-b80125cb3fa7ab12b5921ee930c04e9d95384861.tar.bz2 scala-b80125cb3fa7ab12b5921ee930c04e9d95384861.zip |
Quite a lot more work on completion.
completion is now avilable, with some caveats. Review by community.
9 files changed, 270 insertions, 93 deletions
diff --git a/src/compiler/scala/tools/nsc/Interpreter.scala b/src/compiler/scala/tools/nsc/Interpreter.scala index c98b54304b..406761eddb 100644 --- a/src/compiler/scala/tools/nsc/Interpreter.scala +++ b/src/compiler/scala/tools/nsc/Interpreter.scala @@ -208,7 +208,18 @@ class Interpreter(val settings: Settings, out: PrintWriter) { private val usedNameMap = new HashMap[Name, Request]() private val boundNameMap = new HashMap[Name, Request]() private def allHandlers = prevRequests.toList flatMap (_.handlers) - private def mostRecentHandler = prevRequests.last.handlers.last + + /** Most recent handler which wasn't wholly synthetic. */ + private def mostRecentHandler: MemberHandler = { + for { + req <- prevRequests.reverse + handler <- req.handlers.reverse + name <- handler.generatesValue + if !isSynthVarName(name) + } return handler + + error("No handlers found.") + } def recordRequest(req: Request) { def tripart[T](set1: Set[T], set2: Set[T]) = { @@ -1055,7 +1066,7 @@ class Interpreter(val settings: Settings, out: PrintWriter) { } def clazzForIdent(id: String): Option[Class[_]] = - extractionValueForIdent(id) map (_.getClass) + extractionValueForIdent(id) flatMap (x => Option(x) map (_.getClass)) private def methodsCode(name: String) = "%s.%s(%s)".format(classOf[ReflectionCompletion].getName, "methodsOf", name) diff --git a/src/compiler/scala/tools/nsc/InterpreterLoop.scala b/src/compiler/scala/tools/nsc/InterpreterLoop.scala index ab643def53..afc3247170 100644 --- a/src/compiler/scala/tools/nsc/InterpreterLoop.scala +++ b/src/compiler/scala/tools/nsc/InterpreterLoop.scala @@ -513,18 +513,21 @@ class InterpreterLoop(in0: Option[BufferedReader], out: PrintWriter) { /** Here we place ourselves between the user and the interpreter and examine * the input they are ostensibly submitting. We intervene in several cases: * - * 1) If the line starts with "." it is treated as an invocation on the last result. - * 2) If the line starts with "scala> " it is assumed to be an interpreter paste. + * 1) If the line starts with "scala> " it is assumed to be an interpreter paste. + * 2) If the line starts with "." (but not ".." or "./") it is treated as an invocation + * on the previous result. * 3) If the Completion object's execute returns Some(_), we inject that value * and avoid the interpreter, as it's likely not valid scala code. */ if (code == "") None - else if (code startsWith ".") interpretStartingWith(interpreter.mostRecentVar + code) else if (code startsWith PROMPT_STRING) { updatePasteStamp() interpretAsPastedTranscript(List(code)) None } + else if (Completion.looksLikeInvocation(code)) { + interpretStartingWith(interpreter.mostRecentVar + code) + } else { val result = for (comp <- in.completion ; res <- comp execute code) yield res result match { diff --git a/src/compiler/scala/tools/nsc/interpreter/Completion.scala b/src/compiler/scala/tools/nsc/interpreter/Completion.scala index f9e6b33763..6ca108d773 100644 --- a/src/compiler/scala/tools/nsc/interpreter/Completion.scala +++ b/src/compiler/scala/tools/nsc/interpreter/Completion.scala @@ -22,13 +22,24 @@ import jline._ import java.net.URL import java.util.{ List => JList } import java.lang.reflect +import io.{ Path, Directory } + +object Completion { + def looksLikeInvocation(code: String) = ( + (code != null) + && (code startsWith ".") + && !(code startsWith "./") + && !(code startsWith "..") + ) -trait ForwardingCompletion extends CompletionAware { - def forwardTo: Option[CompletionAware] + trait Forwarder extends CompletionAware { + def forwardTo: Option[CompletionAware] - override def completions() = forwardTo map (_.completions()) getOrElse Nil - override def follow(s: String) = forwardTo flatMap (_ follow s) + override def completions() = forwardTo map (_.completions()) getOrElse Nil + override def follow(s: String) = forwardTo flatMap (_ follow s) + } } +import Completion._ // REPL completor - queries supplied interpreter for valid // completions based on current contents of buffer. @@ -59,7 +70,7 @@ class Completion(repl: Interpreter) { ) } // members of scala.* - val scalalang = new pkgs.SubCompletor("scala") with ForwardingCompletion { + val scalalang = new pkgs.SubCompletor("scala") with Forwarder { def forwardTo = pkgs follow "scala" val arityClasses = { val names = List("Tuple", "Product", "Function") @@ -69,15 +80,15 @@ class Completion(repl: Interpreter) { } override def filterNotFunction(s: String) = { - val parsed = new Parsed(s) + val simple = s.reverse takeWhile (_ != '.') reverse - (arityClasses contains parsed.unqualifiedPart) || + (arityClasses contains simple) || (s endsWith "Exception") || (s endsWith "Error") } } // members of java.lang.* - val javalang = new pkgs.SubCompletor("java.lang") with ForwardingCompletion { + val javalang = new pkgs.SubCompletor("java.lang") with Forwarder { def forwardTo = pkgs follow "java.lang" import reflect.Modifier.isPublic private def existsAndPublic(s: String): Boolean = { @@ -99,19 +110,34 @@ class Completion(repl: Interpreter) { val parent = self } + def lastResult = new Forwarder { + def forwardTo = ids follow repl.mostRecentVar + } + + def lastResultFor(parsed: Parsed) = { + /** The logic is a little tortured right now because normally '.' is + * ignored as a delimiter, but on .<tab> it needs to be propagated. + */ + val xs = lastResult completionsFor parsed + if (parsed.isEmpty) xs map ("." + _) else xs + } + // the list of completion aware objects which should be consulted val topLevel: List[CompletionAware] = List(ids, pkgs, predef, scalalang, javalang, literals) - def topLevelFor(buffer: String) = topLevel flatMap (_ completionsFor buffer) + + // the first tier of top level objects (doesn't include file completion) + def topLevelFor(parsed: Parsed) = topLevel flatMap (_ completionsFor parsed) // chasing down results which won't parse def execute(line: String): Option[Any] = { - val parsed = new Parsed(line) - import parsed._ + val parsed = Parsed(line) + def noDotOrSlash = line forall (ch => ch != '.' && ch != '/') - if (!isQualified) None + if (noDotOrSlash) None // we defer all unqualified ids to the repl. else { - (ids executionFor buffer) orElse - (pkgs executionFor buffer) + (ids executionFor parsed) orElse + (pkgs executionFor parsed) orElse + (FileCompletion executionFor line) } } @@ -119,21 +145,8 @@ class Completion(repl: Interpreter) { def lastCommand: Option[String] = None // jline's entry point - lazy val jline: ArgumentCompletor = { - // TODO - refine the delimiters - // - // public static interface ArgumentDelimiter { - // ArgumentList delimit(String buffer, int argumentPosition); - // boolean isDelimiter(String buffer, int pos); - // } - val delimiters = new ArgumentCompletor.AbstractArgumentDelimiter { - // val delimChars = "(){},`; \t".toArray - val delimChars = "{},`; \t".toArray - def isDelimiterChar(s: String, pos: Int) = delimChars contains s.charAt(pos) - } - - returning(new ArgumentCompletor(new JLineCompletion, delimiters))(_ setStrict false) - } + lazy val jline: ArgumentCompletor = + returning(new ArgumentCompletor(new JLineCompletion, new JLineDelimiter))(_ setStrict false) class JLineCompletion extends Completor { // For recording the buffer on the last tab hit @@ -147,21 +160,34 @@ class Completion(repl: Interpreter) { private var verbosity = 0 // This is jline's entry point for completion. - override def complete(_buffer: String, cursor: Int, candidates: JList[String]): Int = { + override def complete(buf: String, cursor: Int, candidates: JList[String]): Int = { + // println("complete: buf = %s, cursor = %d".format(buf, cursor)) if (!isInitialized) return cursor - // println("_buffer = %s, cursor = %d".format(_buffer, cursor)) - verbosity = if (isConsecutiveTabs(_buffer)) verbosity + 1 else 0 - lastTab = (_buffer, lastCommand orNull) - - // parse the command buffer - val parsed = new Parsed(_buffer) - import parsed._ - - // modify in place and return the position - topLevelFor(buffer) foreach (candidates add _) - position + verbosity = if (isConsecutiveTabs(buf)) verbosity + 1 else 0 + lastTab = (buf, lastCommand orNull) + + // we don't try lower priority completions unless higher ones return no results. + def tryCompletion(p: Parsed, completionFunction: Parsed => List[String]): Option[Int] = { + completionFunction(p) match { + case Nil => None + case xs => + // modify in place and return the position + xs foreach (candidates add _) + Some(p.position) + } + } + + // a single dot is special cased to completion on the previous result + def lastResultCompletion = + if (!looksLikeInvocation(buf)) None + else tryCompletion(Parsed.dotted(buf drop 1, cursor), lastResultFor) + + def regularCompletion = tryCompletion(Parsed.dotted(buf, cursor), topLevelFor) + def fileCompletion = tryCompletion(Parsed.undelimited(buf, cursor), FileCompletion completionsFor _.buffer) + + (lastResultCompletion orElse regularCompletion orElse fileCompletion) getOrElse cursor } } } diff --git a/src/compiler/scala/tools/nsc/interpreter/CompletionAware.scala b/src/compiler/scala/tools/nsc/interpreter/CompletionAware.scala index 91b7a77ce5..bdb390c9fc 100644 --- a/src/compiler/scala/tools/nsc/interpreter/CompletionAware.scala +++ b/src/compiler/scala/tools/nsc/interpreter/CompletionAware.scala @@ -12,6 +12,12 @@ import scala.reflect.NameTransformer * will supply their own candidates and resolve their own paths. */ trait CompletionAware { + /** The delimiters which are meaningful when this CompletionAware + * object is in control. + */ + // TODO + // def delimiters(): List[Char] = List('.') + /** The complete list of unqualified Strings to which this * object will complete. */ @@ -45,39 +51,43 @@ trait CompletionAware { * to which it can complete. This may involve delegating * to other CompletionAware objects. */ - def completionsFor(buf: String): List[String] = { - val parsed = new Parsed(buf) + def completionsFor(parsed: Parsed): List[String] = { import parsed._ - ( + val cs = 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 _) + else if (isUnqualified && !isLastDelimiter) completions(buffer) + else follow(bufferHead) map (_ completionsFor bufferTail) getOrElse Nil + + cs filterNot filterNotFunction map mapFunction sortWith (sortFunction _) } /** TODO - unify this and completionsFor under a common traverser. */ - def executionFor(buf: String): Option[Any] = { - val parsed = new Parsed(buf) + def executionFor(parsed: Parsed): Option[Any] = { import parsed._ - if (isUnqualified && !isLastCharDot && (completions contains buf)) execute(buf) + if (isUnqualified && !isLastDelimiter && (completions contains buffer)) execute(buffer) else if (!isQualified) None - else follow(hd) match { - case Some(next) => next executionFor remainder - case _ => None - } + else follow(bufferHead) flatMap (_ executionFor bufferTail) } } object CompletionAware { val Empty = new CompletionAware { val completions = Nil } + // class Forwarder(underlying: CompletionAware) extends CompletionAware { + // override def completions() = underlying.completions() + // override def filterNotFunction(s: String) = underlying.filterNotFunction(s) + // override def sortFunction(s1: String, s2: String) = underlying.sortFunction(s1, s2) + // override def mapFunction(s: String) = underlying.mapFunction(s) + // override def follow(id: String) = underlying.follow(id) + // override def execute(id: String) = underlying.execute(id) + // override def completionsFor(parsed: Parsed) = underlying.completionsFor(parsed) + // override def executionFor(parsed: Parsed) = underlying.executionFor(parsed) + // } + // + def unapply(that: Any): Option[CompletionAware] = that match { case x: CompletionAware => Some((x)) case _ => None diff --git a/src/compiler/scala/tools/nsc/interpreter/Delimited.scala b/src/compiler/scala/tools/nsc/interpreter/Delimited.scala new file mode 100644 index 0000000000..cdf5a343da --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/Delimited.scala @@ -0,0 +1,36 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +import jline.ArgumentCompletor.{ ArgumentDelimiter, ArgumentList } + +class JLineDelimiter extends ArgumentDelimiter { + def delimit(buffer: String, cursor: Int) = Parsed(buffer, cursor).asJlineArgumentList + def isDelimiter(buffer: String, cursor: Int) = Parsed(buffer, cursor).isDelimiter +} + +trait Delimited { + self: Parsed => + + def delimited: Char => Boolean + def escapeChars: List[Char] = List('\\') + def quoteChars: List[(Char, Char)] = List(('\'', '\''), ('"', '"')) + + /** Break String into args based on delimiting function. + */ + protected def toArgs(s: String): List[String] = + if (s == "") Nil + else (s indexWhere isDelimiterChar) match { + case -1 => List(s) + case idx => (s take idx) :: toArgs(s drop (idx + 1)) + } + + def isDelimiterChar(ch: Char) = delimited(ch) + def isEscapeChar(ch: Char): Boolean = escapeChars contains ch + def isQuoteStart(ch: Char): Boolean = quoteChars map (_._1) contains ch + def isQuoteEnd(ch: Char): Boolean = quoteChars map (_._2) contains ch +} diff --git a/src/compiler/scala/tools/nsc/interpreter/FileCompletion.scala b/src/compiler/scala/tools/nsc/interpreter/FileCompletion.scala new file mode 100644 index 0000000000..c564562a63 --- /dev/null +++ b/src/compiler/scala/tools/nsc/interpreter/FileCompletion.scala @@ -0,0 +1,54 @@ +/* NSC -- new Scala compiler + * Copyright 2005-2010 LAMP/EPFL + * @author Paul Phillips + */ + +package scala.tools.nsc +package interpreter + +/** TODO + * Spaces, dots, and other things in filenames are not correctly handled. + * space-escaping, knowing when we're inside quotes, etc. would be nice. + */ + +import io.{ Directory, Path } + +/** This isn't 100% clean right now, but it works and is simple. Rather + * than delegate to new objects on each '/' in the path, we treat the + * buffer like a path and process it directly. + */ +object FileCompletion { + def executionFor(buffer: String): Option[Path] = { + val p = Path(buffer) + if (p.exists) Some(p) else None + } + + private def fileCompletionForwarder(buffer: String, where: Directory): List[String] = { + completionsFor(where.path + buffer) map (_ stripPrefix where.path) toList + } + + private def homeCompletions(buffer: String): List[String] = { + require(buffer startsWith "~/") + val home = Directory.Home getOrElse (return Nil) + fileCompletionForwarder(buffer.tail, home) map ("~" + _) + } + private def cwdCompletions(buffer: String): List[String] = { + require(buffer startsWith "./") + val cwd = Directory.Current getOrElse (return Nil) + fileCompletionForwarder(buffer.tail, cwd) map ("." + _) + } + + def completionsFor(buffer: String): List[String] = + if (buffer startsWith "~/") homeCompletions(buffer) + else if (buffer startsWith "./") cwdCompletions(buffer) + else { + val p = Path(buffer) + val (dir, stub) = + // don't want /foo/. expanding "." + if (p.name == ".") (p.parent, ".") + else if (p.isDirectory) (p.toDirectory, "") + else (p.parent, p.name) + + dir.list filter (_.name startsWith stub) map (_.path) toList + } +}
\ No newline at end of file diff --git a/src/compiler/scala/tools/nsc/interpreter/PackageCompletion.scala b/src/compiler/scala/tools/nsc/interpreter/PackageCompletion.scala index 4e1d16c87c..a0d83e1880 100644 --- a/src/compiler/scala/tools/nsc/interpreter/PackageCompletion.scala +++ b/src/compiler/scala/tools/nsc/interpreter/PackageCompletion.scala @@ -58,6 +58,7 @@ class PackageCompletion(classpath: List[URL]) extends CompletionAware { aliasCompletor(root + "." + segment) } } + override def toString = "SubCompletor(%s)" format root } } diff --git a/src/compiler/scala/tools/nsc/interpreter/Parsed.scala b/src/compiler/scala/tools/nsc/interpreter/Parsed.scala index 885893ee53..05cb2641cd 100644 --- a/src/compiler/scala/tools/nsc/interpreter/Parsed.scala +++ b/src/compiler/scala/tools/nsc/interpreter/Parsed.scala @@ -6,26 +6,59 @@ package scala.tools.nsc package interpreter +import jline.ArgumentCompletor.{ ArgumentDelimiter, ArgumentList } + /** 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 unqualifiedPart = segments.last - - 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 +class Parsed private ( + val buffer: String, + val cursor: Int, + val delimited: Char => Boolean +) extends Delimited { + def isEmpty = buffer == "" + def isUnqualified = args.size == 1 + def isQualified = args.size > 1 + def isAtStart = cursor <= 0 + + def args = toArgs(buffer take cursor).toList + def bufferHead = args.head + def headLength = bufferHead.length + 1 + def bufferTail = new Parsed(buffer drop headLength, cursor - headLength, delimited) + + def prev = new Parsed(buffer, cursor - 1, delimited) + def next = new Parsed(buffer, cursor + 1, delimited) + def currentChar = buffer(cursor) + def currentArg = args.last + def position = + if (isEmpty) 0 + else if (isLastDelimiter) cursor + else cursor - currentArg.length + + def isFirstDelimiter = !isEmpty && isDelimiterChar(buffer.head) + def isLastDelimiter = !isEmpty && isDelimiterChar(buffer.last) + def firstIfDelimiter = if (isFirstDelimiter) buffer.head.toString else "" + def lastIfDelimiter = if (isLastDelimiter) buffer.last.toString else "" + + def isQuoted = false // TODO + def isEscaped = !isAtStart && isEscapeChar(currentChar) && !isEscapeChar(prev.currentChar) + def isDelimiter = !isQuoted && !isEscaped && isDelimiterChar(currentChar) + + def asJlineArgumentList = + if (isEmpty) new ArgumentList(Array[String](), 0, 0, cursor) + else new ArgumentList(args.toArray, args.size - 1, currentArg.length, cursor) + + override def toString = "Parsed(%s / %d)".format(buffer, cursor) } +object Parsed { + def onull(s: String) = if (s == null) "" else s + def apply(s: String): Parsed = apply(onull(s), onull(s).length) + def apply(s: String, cursor: Int): Parsed = apply(onull(s), cursor, "(){},`; \t" contains _) + def apply(s: String, cursor: Int, delimited: Char => Boolean): Parsed = + new Parsed(onull(s), cursor, delimited) + + def dotted(s: String): Parsed = dotted(onull(s), onull(s).length) + def dotted(s: String, cursor: Int): Parsed = new Parsed(onull(s), cursor, _ == '.') + + def undelimited(s: String, cursor: Int): Parsed = new Parsed(onull(s), cursor, _ => false) +} diff --git a/src/compiler/scala/tools/nsc/io/Path.scala b/src/compiler/scala/tools/nsc/io/Path.scala index de1d129e00..b3f7ea5ab5 100644 --- a/src/compiler/scala/tools/nsc/io/Path.scala +++ b/src/compiler/scala/tools/nsc/io/Path.scala @@ -92,6 +92,7 @@ class Path private[io] (val jfile: JFile) def name: String = jfile.getName() def path: String = jfile.getPath() def normalize: Path = Path(jfile.getCanonicalPath()) + def isRootPath: Boolean = roots exists (_ isSame this) def resolve(other: Path) = if (other.isAbsolute || isEmpty) other else /(other) def relativize(other: Path) = { @@ -113,17 +114,19 @@ class Path private[io] (val jfile: JFile) /** * @return The path of the parent directory, or root if path is already root */ - def parent: Path = { - val p = path match { - case "" | "." => ".." - case _ if path endsWith ".." => path + separator + ".." // the only solution - case _ => jfile.getParent match { - case null if isAbsolute => path // it should be a root. BTW, don't need to worry about relative pathed root - case null => "." // a file ot dir under pwd - case x => x - } - } - new Directory(new JFile(p)) + def parent: Directory = path match { + case "" | "." => Directory("..") + case _ => + // the only solution <-- a comment which could have used elaboration + if (segments.nonEmpty && segments.last == "..") + (path / "..").toDirectory + else jfile.getParent match { + case null => + if (isAbsolute) toDirectory // it should be a root. BTW, don't need to worry about relative pathed root + else Directory(".") // a dir under pwd + case x => + Directory(x) + } } def parents: List[Path] = { val p = parent |