diff options
author | Adriaan Moors <adriaan@lightbend.com> | 2017-02-07 11:55:00 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-02-07 11:55:00 -0800 |
commit | 575c342039475d3c319487da81df5654ecf1127d (patch) | |
tree | 101b953fd17dad0040cf09fb7f6f0bbaa6a2a7c5 /src | |
parent | 7311a29a992db1d3d16a73a9c49c80b0a4383103 (diff) | |
parent | c994b59d0da0cfbad9a2a8c2148dc2328289a3d4 (diff) | |
download | scala-575c342039475d3c319487da81df5654ecf1127d.tar.gz scala-575c342039475d3c319487da81df5654ecf1127d.tar.bz2 scala-575c342039475d3c319487da81df5654ecf1127d.zip |
Merge pull request #5665 from som-snytt/issue/8662-file-complete
SI-8662 Repl completions
Diffstat (limited to 'src')
-rw-r--r-- | src/repl/scala/tools/nsc/interpreter/ILoop.scala | 113 | ||||
-rw-r--r-- | src/repl/scala/tools/nsc/interpreter/LoopCommands.scala | 84 |
2 files changed, 141 insertions, 56 deletions
diff --git a/src/repl/scala/tools/nsc/interpreter/ILoop.scala b/src/repl/scala/tools/nsc/interpreter/ILoop.scala index d1096fda4e..d03924b158 100644 --- a/src/repl/scala/tools/nsc/interpreter/ILoop.scala +++ b/src/repl/scala/tools/nsc/interpreter/ILoop.scala @@ -9,6 +9,7 @@ package interpreter import scala.language.{ implicitConversions, existentials } import scala.annotation.tailrec import Predef.{ println => _, _ } +import PartialFunction.{cond => when} import interpreter.session._ import StdReplTags._ import scala.tools.asm.ClassReader @@ -17,7 +18,7 @@ import scala.tools.nsc.util.{ ClassPath, stringFromStream } import scala.reflect.classTag import scala.reflect.internal.util.{ BatchSourceFile, ScalaClassLoader, NoPosition } import ScalaClassLoader._ -import scala.reflect.io.File +import scala.reflect.io.{Directory, File, Path} import scala.tools.util._ import io.AbstractFile import scala.concurrent.{ ExecutionContext, Await, Future } @@ -26,6 +27,8 @@ import java.io.BufferedReader import scala.util.{ Try, Success, Failure } +import Completion._ + /** The Scala interactive shell. It provides a read-eval-print loop * around the Interpreter class. * After instantiation, clients should call the main() method. @@ -119,37 +122,6 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) intp = new ILoopInterpreter } - /** print a friendly help message */ - def helpCommand(line: String): Result = line match { - case "" => helpSummary() - case CommandMatch(cmd) => echo(f"%n${cmd.help}") - case _ => ambiguousError(line) - } - private def helpSummary() = { - val usageWidth = commands map (_.usageMsg.length) max - val formatStr = s"%-${usageWidth}s %s" - - echo("All commands can be abbreviated, e.g., :he instead of :help.") - - for (cmd <- commands) echo(formatStr.format(cmd.usageMsg, cmd.help)) - } - private def ambiguousError(cmd: String): Result = { - matchingCommands(cmd) match { - case Nil => echo(cmd + ": no such command. Type :help for help.") - case xs => echo(cmd + " is ambiguous: did you mean " + xs.map(":" + _.name).mkString(" or ") + "?") - } - Result(keepRunning = true, None) - } - // this lets us add commands willy-nilly and only requires enough command to disambiguate - private def matchingCommands(cmd: String) = commands filter (_.name startsWith cmd) - private object CommandMatch { - def unapply(name: String): Option[LoopCommand] = - matchingCommands(name) match { - case x :: Nil => Some(x) - case xs => xs find (_.name == name) // accept an exact match - } - } - /** Show the history */ lazy val historyCommand = new LoopCommand("history", "show the history (optional num is commands to show)") { override def usage = "[num]" @@ -214,16 +186,16 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) cmd("implicits", "[-v]", "show the implicits in scope", intp.implicitsCommand), cmd("javap", "<path|class>", "disassemble a file or class name", javapCommand), cmd("line", "<id>|<line>", "place line(s) at the end of history", lineCommand), - cmd("load", "<path>", "interpret lines in a file", loadCommand), - cmd("paste", "[-raw] [path]", "enter paste mode or paste a file", pasteCommand), + cmd("load", "<path>", "interpret lines in a file", loadCommand, fileCompletion), + cmd("paste", "[-raw] [path]", "enter paste mode or paste a file", pasteCommand, fileCompletion), nullary("power", "enable power user mode", powerCmd), nullary("quit", "exit the interpreter", () => Result(keepRunning = false, None)), - cmd("replay", "[options]", "reset the repl and replay all previous commands", replayCommand), + cmd("replay", "[options]", "reset the repl and replay all previous commands", replayCommand, settingsCompletion), cmd("require", "<path>", "add a jar to the classpath", require), - cmd("reset", "[options]", "reset the repl to its initial state, forgetting all session entries", resetCommand), - cmd("save", "<path>", "save replayable session to a file", saveCommand), + cmd("reset", "[options]", "reset the repl to its initial state, forgetting all session entries", resetCommand, settingsCompletion), + cmd("save", "<path>", "save replayable session to a file", saveCommand, fileCompletion), shCommand, - cmd("settings", "<options>", "update compiler options, if possible; see reset", changeSettings), + cmd("settings", "<options>", "update compiler options, if possible; see reset", changeSettings, settingsCompletion), nullary("silent", "disable/enable automatic printing of results", verbosity), cmd("type", "[-v] <expr>", "display the type of an expression without evaluating it", typeCommand), cmd("kind", "[-v] <expr>", "display the kind of expression's type", kindCommand), @@ -235,6 +207,46 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) cmd("phase", "<phase>", "set the implicit phase for power commands", phaseCommand) ) + // complete filename + val fileCompletion: Completion = new Completion { + def resetVerbosity(): Unit = () + val emptyWord = """(\s+)$""".r.unanchored + val directorily = """(\S*/)$""".r.unanchored + val trailingWord = """(\S+)$""".r.unanchored + def listed(i: Int, dir: Option[Path]) = + dir.filter(_.isDirectory).map(d => Candidates(i, d.toDirectory.list.map(_.name).toList)).getOrElse(NoCandidates) + def listedIn(dir: Directory, name: String) = dir.list.filter(_.name.startsWith(name)).map(_.name).toList + def complete(buffer: String, cursor: Int): Candidates = + buffer.substring(0, cursor) match { + case emptyWord(s) => listed(cursor, Directory.Current) + case directorily(s) => listed(cursor, Option(Path(s))) + case trailingWord(s) => + val f = File(s) + val (i, maybes) = + if (f.isFile) (cursor - s.length, List(f.toAbsolute.path)) + else if (f.isDirectory) (cursor - s.length, List(s"${f.toAbsolute.path}/")) + else if (f.parent.exists) (cursor - f.name.length, listedIn(f.parent.toDirectory, f.name)) + else (-1, Nil) + if (maybes.isEmpty) NoCandidates else Candidates(i, maybes) + case _ => NoCandidates + } + } + + // complete settings name + val settingsCompletion: Completion = new Completion { + def resetVerbosity(): Unit = () + val trailingWord = """(\S+)$""".r.unanchored + def complete(buffer: String, cursor: Int): Candidates = { + buffer.substring(0, cursor) match { + case trailingWord(s) => + val maybes = settings.visibleSettings.filter(_.name.startsWith(s)).map(_.name) + .filterNot(when(_) { case "-"|"-X"|"-Y" => true }).toList.sorted + if (maybes.isEmpty) NoCandidates else Candidates(cursor - s.length, maybes) + case _ => NoCandidates + } + } + } + private def importsCommand(line: String): Result = { val tokens = words(line) val handlers = intp.languageWildcardHandlers ++ intp.importHandlers @@ -681,20 +693,11 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) * (1) whether to keep running, (2) the line to record for replay, if any. */ def command(line: String): Result = { - if (line startsWith ":") colonCommand(line.tail) + if (line startsWith ":") colonCommand(line) else if (intp.global == null) Result(keepRunning = false, None) // Notice failure to create compiler else Result(keepRunning = true, interpretStartingWith(line)) } - private val commandish = """(\S+)(?:\s+)?(.*)""".r - - private def colonCommand(line: String): Result = line.trim match { - case "" => helpSummary() - case commandish(CommandMatch(cmd), rest) => cmd(rest) - case commandish(name, _) => ambiguousError(name) - case _ => echo("?") - } - private def readWhile(cond: String => Boolean) = { Iterator continually in.readLine("") takeWhile (x => x != null && cond(x)) } @@ -849,6 +852,18 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) } } + // delegate to command completion or presentation compiler + class ReplCompletion(intp: IMain) extends Completion { + val pc = new PresentationCompilerCompleter(intp) + def resetVerbosity(): Unit = pc.resetVerbosity() + def complete(buffer: String, cursor: Int): Completion.Candidates = { + if (buffer.startsWith(":")) + colonCompletion(buffer, cursor).complete(buffer, cursor) + else + pc.complete(buffer, cursor) + } + } + /** Tries to create a jline.InteractiveReader, falling back to SimpleReader, * unless settings or properties are such that it should start with SimpleReader. * The constructor of the InteractiveReader must take a Completion strategy, @@ -868,7 +883,7 @@ class ILoop(in0: Option[BufferedReader], protected val out: JPrintWriter) } def mkReader(maker: ReaderMaker) = maker { () => - if (settings.noCompletion) NoCompletion else new PresentationCompilerCompleter(intp) + if (settings.noCompletion) NoCompletion else new ReplCompletion(intp) } def internalClass(kind: String) = s"scala.tools.nsc.interpreter.$kind.InteractiveReader" diff --git a/src/repl/scala/tools/nsc/interpreter/LoopCommands.scala b/src/repl/scala/tools/nsc/interpreter/LoopCommands.scala index 8c3b174513..a2ce63996b 100644 --- a/src/repl/scala/tools/nsc/interpreter/LoopCommands.scala +++ b/src/repl/scala/tools/nsc/interpreter/LoopCommands.scala @@ -24,18 +24,21 @@ class ProcessResult(val line: String) { override def toString = "`%s` (%d lines, exit %d)".format(line, buffer.size, exitCode) } -trait LoopCommands { +trait LoopCommands { self: { def echo(msg: String): Unit } => protected def out: JPrintWriter // So outputs can be suppressed. - def echoCommandMessage(msg: String): Unit = out println msg + def echoCommandMessage(msg: String): Unit = out.println(msg) + + // available commands + def commands: List[LoopCommand] // a single interpreter command abstract class LoopCommand(val name: String, val help: String) extends (String => Result) { def usage: String = "" - def usageMsg: String = ":" + name + ( + def usageMsg: String = s":$name${ if (usage == "") "" else " " + usage - ) + }" def apply(line: String): Result // called if no args are given @@ -43,21 +46,88 @@ trait LoopCommands { "usage is " + usageMsg Result(keepRunning = true, None) } + + // subclasses may provide completions + def completion: Completion = NoCompletion } object LoopCommand { def nullary(name: String, help: String, f: () => Result): LoopCommand = new NullaryCmd(name, help, _ => f()) - def cmd(name: String, usage: String, help: String, f: String => Result): LoopCommand = + def cmd(name: String, usage: String, help: String, f: String => Result, completion: Completion = NoCompletion): LoopCommand = if (usage == "") new NullaryCmd(name, help, f) - else new LineCmd(name, usage, help, f) + else new LineCmd(name, usage, help, f, completion) + } + + /** print a friendly help message */ + def helpCommand(line: String): Result = line match { + case "" => helpSummary() + case CommandMatch(cmd) => echo(f"%n${cmd.help}") + case _ => ambiguousError(line) + } + + def helpSummary() = { + val usageWidth = commands map (_.usageMsg.length) max + val formatStr = s"%-${usageWidth}s %s" + + echo("All commands can be abbreviated, e.g., :he instead of :help.") + + for (cmd <- commands) echo(formatStr.format(cmd.usageMsg, cmd.help)) + } + def ambiguousError(cmd: String): Result = { + matchingCommands(cmd) match { + case Nil => echo(cmd + ": no such command. Type :help for help.") + case xs => echo(cmd + " is ambiguous: did you mean " + xs.map(":" + _.name).mkString(" or ") + "?") + } + Result(keepRunning = true, None) + } + + // all commands with given prefix + private def matchingCommands(cmd: String) = commands.filter(_.name.startsWith(cmd.stripPrefix(":"))) + + // extract command from partial name, or prefer exact match if multiple matches + private object CommandMatch { + def unapply(name: String): Option[LoopCommand] = + matchingCommands(name) match { + case Nil => None + case x :: Nil => Some(x) + case xs => xs find (_.name == name) + } + } + + // extract command name and rest of line + private val commandish = """(\S+)(?:\s+)?(.*)""".r + + def colonCommand(line: String): Result = line.trim match { + case "" => helpSummary() + case commandish(CommandMatch(cmd), rest) => cmd(rest) + case commandish(name, _) => ambiguousError(name) + case _ => echo("?") + } + + import Completion.Candidates + + def colonCompletion(line: String, cursor: Int): Completion = line.trim match { + case commandish(name @ CommandMatch(cmd), rest) => + if (name.length > cmd.name.length) cmd.completion + else + new Completion { + def resetVerbosity(): Unit = () + def complete(buffer: String, cursor: Int) = Candidates(cursor - name.length + 1, List(cmd.name)) + } + case commandish(name, _) if matchingCommands(name).nonEmpty => + new Completion { + def resetVerbosity(): Unit = () + def complete(buffer: String, cursor: Int) = Candidates(cursor - name.length + 1, matchingCommands(name).map(_.name)) + } + case _ => NoCompletion } class NullaryCmd(name: String, help: String, f: String => Result) extends LoopCommand(name, help) { def apply(line: String): Result = f(line) } - class LineCmd(name: String, argWord: String, help: String, f: String => Result) extends LoopCommand(name, help) { + class LineCmd(name: String, argWord: String, help: String, f: String => Result, override val completion: Completion) extends LoopCommand(name, help) { override def usage = argWord def apply(line: String): Result = f(line) } |