diff --git a/main/Command.scala b/main/Command.scala index b33465fba..94ba9e164 100644 --- a/main/Command.scala +++ b/main/Command.scala @@ -3,22 +3,113 @@ */ package sbt - import Execute.NodeView import java.io.File - import Function.untupled - import parse.Parser + import complete.{DefaultParsers, Parser} + import CommandSupport.logger -trait NewCommand // to replace Command -{ - type T - def parser: State => Option[Parser[T]] - def run: (T, State) => State +sealed trait Command { + def help: Seq[Help] + def parser: State => Parser[State] + def tags: AttributeMap + def tag[T](key: AttributeKey[T], value: T): Command } -trait Command -{ - def help: State => Seq[Help] - def run: (Input, State) => Option[State] +private[sbt] final class SimpleCommand(val name: String, val help: Seq[Help], val parser: State => Parser[State], val tags: AttributeMap) extends Command { + assert(Command validID name, "'" + name + "' is not a valid command name." ) + def tag[T](key: AttributeKey[T], value: T): SimpleCommand = new SimpleCommand(name, help, parser, tags.put(key, value)) } +private[sbt] final class ArbitraryCommand(val parser: State => Parser[State], val help: Seq[Help], val tags: AttributeMap) extends Command +{ + def tag[T](key: AttributeKey[T], value: T): ArbitraryCommand = new ArbitraryCommand(parser, help, tags.put(key, value)) +} + +object Command +{ + def pointer(s: String, i: Int): String = (s take i) map { case '\t' => '\t'; case _ => ' ' } mkString; + + import DefaultParsers._ + + val Logged = AttributeKey[Logger]("log") + val HistoryPath = SettingKey[Option[File]]("history") + val Analysis = AttributeKey[inc.Analysis]("analysis") + val Watch = SettingKey[Watched]("continuous-watch") + + def command(name: String)(f: State => State): Command = command(name, Nil)(f) + def command(name: String, briefHelp: String, detail: String)(f: State => State): Command = command(name, Help(name, (name, briefHelp), detail) :: Nil)(f) + def command(name: String, help: Seq[Help])(f: State => State): Command = apply(name, help : _*)(state => success(f(state))) + + def apply(name: String, briefHelp: (String, String), detail: String)(parser: State => Parser[State]): Command = + apply(name, Help(name, briefHelp, detail) )(parser) + def apply(name: String, help: Help*)(parser: State => Parser[State]): Command = new SimpleCommand(name, help, parser, AttributeMap.empty) + + def args(name: String, briefHelp: (String, String), detail: String, display: String)(f: (State, Seq[String]) => State): Command = + args(name, display, Help(name, briefHelp, detail) )(f) + + def args(name: String, display: String, help: Help*)(f: (State, Seq[String]) => State): Command = + apply(name, help : _*)( state => spaceDelimited(display) map f.curried(state) ) + + def single(name: String, briefHelp: (String, String), detail: String)(f: (State, String) => State): Command = + single(name, Help(name, briefHelp, detail) )(f) + def single(name: String, help: Help*)(f: (State, String) => State): Command = + apply(name, help : _*)( state => token(any.+.string map f.curried(state)) ) + + def custom(parser: State => Parser[State], help: Seq[Help]): Command = new ArbitraryCommand(parser, help, AttributeMap.empty) + + def validID(name: String) = + Parser(OpOrID)(name).resultEmpty.isDefined + + def combine(cmds: Seq[Command]): State => Parser[State] = + { + val (simple, arbs) = separateCommands(cmds) + state => (simpleParser(simple)(state) /: arbs.map(_ parser state) ){ _ | _ } + } + private[this] def separateCommands(cmds: Seq[Command]): (Seq[SimpleCommand], Seq[ArbitraryCommand]) = + Collections.separate(cmds){ case s: SimpleCommand => Left(s); case a: ArbitraryCommand => Right(a) } + + def simpleParser(cmds: Seq[SimpleCommand]): State => Parser[State] = + simpleParser(cmds.map(sc => (sc.name, sc.parser)).toMap ) + + def simpleParser(commandMap: Map[String, State => Parser[State]]): State => Parser[State] = + (state: State) => token(OpOrID examples commandMap.keys.toSet) flatMap { id => + (commandMap get id) match { case None => failure("No command named '" + id + "'"); case Some(c) => c(state) } + } + + def process(command: String, state: State): State = + { + val parser = combine(state.processors) + Parser.result(parser(state), command) match + { + case Right(s) => s + case Left((msg,pos)) => + val errMsg = commandError(command, msg, pos) + logger(state).info(errMsg) + state.fail + } + } + def commandError(command: String, msg: String, index: Int): String = + { + val (line, modIndex) = extractLine(command, index) + msg + "\n" + line + "\n" + pointer(msg, modIndex) + } + def extractLine(s: String, i: Int): (String, Int) = + { + val notNewline = (c: Char) => c != '\n' && c != '\r' + val left = takeRightWhile( s.substring(0, i) )( notNewline ) + val right = s substring i takeWhile notNewline + (left + right, left.length) + } + def takeRightWhile(s: String)(pred: Char => Boolean): String = + { + def loop(i: Int): String = + if(i < 0) + s + else if( pred(s(i)) ) + loop(i-1) + else + s.substring(i+1) + loop(s.length - 1) + } +} + trait Help { def detail: (Set[String], String) @@ -26,43 +117,11 @@ trait Help } object Help { + def apply(name: String, briefHelp: (String, String), detail: String): Help = apply(briefHelp, (Set(name), detail)) + def apply(briefHelp: (String, String), detailedHelp: (Set[String], String) = (Set.empty, "") ): Help = new Help { def detail = detailedHelp; def brief = briefHelp } } -object Command -{ - val Logged = AttributeKey[Logger]("log") - val HistoryPath = SettingKey[Option[File]]("history") - val Analysis = AttributeKey[inc.Analysis]("analysis") - val Watch = SettingKey[Watched]("continuous-watch") - - def direct(h: Help*)(r: (Input, State) => Option[State]): Command = - new Command { def help = _ => h; def run = r } - - def apply(h: Help*)(r: PartialFunction[(Input, State), State]): Command = - direct(h : _*)(untupled(r.lift)) - - def simple(name: String, brief: (String, String), detail: String)(f: (Input, State) => State): Command = - { - val h = Help(brief, (Set(name), detail) ) - simple(name, h)(f) - } - def simple(name: String, help: Help*)(f: (Input, State) => State): Command = - Command( help: _* ){ case (in, s) if name == in.name => f( in, s) } -} -final case class Input(line: String, cursor: Option[Int]) -{ - lazy val (name, arguments) = line match { case Input.NameRegex(n, a) => (n, a); case _ => (line, "") } - lazy val splitArgs: Seq[String] = if(arguments.isEmpty) Nil else (arguments split """\s+""").toSeq -} -object Input -{ - val NameRegex = """\s*(\p{Punct}+|[\w-]+)\s*(.*)""".r -} - -object Next extends Enumeration { - val Reload, Fail, Done, Continue = Value -} trait CommandDefinitions { def commands: Seq[Command] diff --git a/main/CommandSupport.scala b/main/CommandSupport.scala index dc1e93bb0..27477e97a 100644 --- a/main/CommandSupport.scala +++ b/main/CommandSupport.scala @@ -34,9 +34,8 @@ object CommandSupport val Exit = "exit" val Quit = "quit" - /** The list of command names that may be used to terminate the program.*/ - val TerminateActions: Seq[String] = Seq(Exit, Quit) - + /** The command name to terminate the program.*/ + val TerminateAction: String = Exit def continuousBriefHelp = (ContinuousExecutePrefix + " ", "Executes the specified command whenever source files change.") @@ -62,12 +61,12 @@ ProjectCommand + For example, 'project ....' is equivalent to three consecutive 'project ..' commands. """ - def projectsBrief = (ProjectsCommand, projectsDetailed) + def projectsBrief = projectsDetailed def projectsDetailed = "Displays the names of available projects." def historyHelp = HistoryCommands.descriptions.map( d => Help(d) ) - def exitBrief = (TerminateActions.mkString(", "), "Terminates the build.") + def exitBrief = (TerminateAction, "Terminates the build.") def sbtrc = ".sbtrc" @@ -92,7 +91,7 @@ ProjectCommand + def DefaultsDetailed = "Registers default built-in commands" def ReloadCommand = "reload" - def ReloadBrief = (ReloadCommand, "Reloads the session and then executes the remaining commands.") + def ReloadBrief = "Reloads the session and then executes the remaining commands." def ReloadDetailed = ReloadCommand + """ This command is equivalent to exiting, restarting, and running the @@ -180,11 +179,11 @@ CompileSyntax + """ def LoadCommandLabel = "commands" def LoadProject = "loadp" - def LoadProjectBrief = (LoadProject, LoadProjectDetailed) + def LoadProjectBrief = LoadProjectDetailed def LoadProjectDetailed = "Loads the project in the current directory" def Shell = "shell" - def ShellBrief = (Shell, ShellDetailed) + def ShellBrief = ShellDetailed def ShellDetailed = "Provides an interactive prompt from which commands can be run." def OnFailure = "-" diff --git a/main/Main.scala b/main/Main.scala index 118be87d2..8f777dad0 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -1,19 +1,22 @@ /* sbt -- Simple Build Tool - * Copyright 2008, 2009, 2010 Mark Harrah + * Copyright 2008, 2009, 2010, 2011 Mark Harrah */ package sbt -import Execute.NodeView -import complete.HistoryCommands -import HistoryCommands.{Start => HistoryPrefix} -import Project.{SessionKey, StructureKey} -import sbt.build.{AggressiveCompile, Auto, BuildException, LoadCommand, Parse, ParseException, ProjectLoad, SourceLoad} -import Command.{Analysis,HistoryPath,Logged,Watch} -import scala.annotation.tailrec -import scala.collection.JavaConversions._ -import Path._ + import Execute.NodeView + import complete.HistoryCommands + import HistoryCommands.{Start => HistoryPrefix} + import Project.{SessionKey, StructureKey} + import sbt.build.{AggressiveCompile, Auto, BuildException, LoadCommand, Parse, ParseException, ProjectLoad, SourceLoad} + import sbt.complete.{DefaultParsers, Parser} -import java.io.File + import Command.{Analysis,HistoryPath,Logged,Watch} + import scala.annotation.tailrec + import scala.collection.JavaConversions._ + import Function.tupled + import Path._ + + import java.io.File /** This class is the entry point for sbt.*/ class xMain extends xsbti.AppMain @@ -43,64 +46,53 @@ class xMain extends xsbti.AppMain } } def next(state: State): State = - ErrorHandling.wideConvert { state.process(process) } match + ErrorHandling.wideConvert { state.process(Command.process) } match { case Right(s) => s case Left(t) => Commands.handleException(t, state) } - def process(command: String, state: State): State = - { - val in = Input(command, None) - Commands.applicable(state).flatMap( _.run(in, state) ).headOption.getOrElse { - if(command.isEmpty) state - else { - System.err.println("Unknown command '" + command + "'") - state.fail - } - } - } } -import CommandSupport._ + import DefaultParsers._ + import CommandSupport._ object Commands { def DefaultCommands: Seq[Command] = Seq(ignore, help, reload, read, history, continuous, exit, loadCommands, loadProject, compile, discover, - projects, project, setOnFailure, ifLast, multi, shell, alias, append) + projects, project, setOnFailure, ifLast, multi, shell, alias, append, nop) - def ignore = nothing(Set(FailureWall)) - - def nothing(ignore: Set[String]) = Command(){ case (in, s) if ignore(in.line) => s } - - def applicable(state: State): Stream[Command] = state.processors.toStream + def nop = Command.custom(successStrict, Nil) + def ignore = Command.command(FailureWall)(identity) def detail(selected: Iterable[String])(h: Help): Option[String] = h.detail match { case (commands, value) => if( selected exists commands ) Some(value) else None } - def help = Command.simple(HelpCommand, helpBrief, helpDetailed) { (in, s) => + // TODO: tab complete on command names + def help = Command.args(HelpCommand, helpBrief, helpDetailed, "") { (s, args) => - val h = applicable(s).flatMap(_.help(s)) - val argStr = (in.line stripPrefix HelpCommand).trim - + val h = s.processors.flatMap(_.help) val message = - if(argStr.isEmpty) + if(args.isEmpty) h.map( _.brief match { case (a,b) => a + " : " + b } ).mkString("\n", "\n", "\n") else - h flatMap detail( argStr.split("""\s+""", 0) ) mkString("\n", "\n\n", "\n") + h flatMap detail(args) mkString("\n", "\n\n", "\n") System.out.println(message) s } - def alias = Command.simple(AliasCommand, AliasBrief, AliasDetailed) { (in, s) => - in.arguments.split("""\s*=\s*""",2).toSeq match { - case Seq(name, value) => addAlias(s, name.trim, value.trim) - case Seq(x) if !x.isEmpty=> printAlias(s, x.trim); s - case _ => printAliases(s); s + def alias = Command(AliasCommand, AliasBrief, AliasDetailed) { s => + val name = token(OpOrID.examples( aliasNames(s) : _*) ) + val assign = token(Space ~ '=' ~ Space) ~> matched(Command.combine(s.processors)(s), partial = true) + (OptSpace ~> (name ~ assign.?).?) map { + case Some((name, Some(value))) => addAlias(s, name.trim, value.trim) + case Some((x, None)) if !x.isEmpty=> printAlias(s, x.trim); s + case None => printAliases(s); s } } - def shell = Command.simple(Shell, ShellBrief, ShellDetailed) { (in, s) => + def shell = Command.command(Shell, ShellBrief, ShellDetailed) { s => val historyPath = (s get HistoryPath.key) getOrElse Some((s.baseDir / ".history").asFile) - val reader = new LazyJLineReader(historyPath) + val parser = Command.combine(s.processors) + val reader = new FullReader(historyPath, parser(s)) val line = reader.readLine("> ") line match { case Some(line) => s.copy(onFailure = Some(Shell), commands = line +: Shell +: s.commands) @@ -108,35 +100,48 @@ object Commands } } - def multi = Command.simple(Multi, MultiBrief, MultiDetailed) { (in, s) => - in.arguments.split(";").toSeq ::: s + // TODO: this should nest Parsers for other commands + def multi = Command.single(Multi, MultiBrief, MultiDetailed) { (s,arg) => + arg.split(";").toSeq ::: s } - def ifLast = Command.simple(IfLast, IfLastBrief, IfLastDetailed) { (in, s) => - if(s.commands.isEmpty) in.arguments :: s else s + // TODO: nest + def ifLast = Command.single(IfLast, IfLastBrief, IfLastDetailed) { (s, arg) => + if(s.commands.isEmpty) arg :: s else s } - def append = Command.simple(Append, AppendLastBrief, AppendLastDetailed) { (in, s) => - s.copy(commands = s.commands :+ in.arguments) + // TODO: nest + def append = Command.single(Append, AppendLastBrief, AppendLastDetailed) { (s, arg) => + s.copy(commands = s.commands :+ arg) } - def setOnFailure = Command.simple(OnFailure, OnFailureBrief, OnFailureDetailed) { (in, s) => - s.copy(onFailure = Some(in.arguments)) + // TODO: nest + def setOnFailure = Command.single(OnFailure, OnFailureBrief, OnFailureDetailed) { (s, arg) => + s.copy(onFailure = Some(arg)) } - def reload = Command.simple(ReloadCommand, ReloadBrief, ReloadDetailed) { (in, s) => + def reload = Command.command(ReloadCommand, ReloadBrief, ReloadDetailed) { s => runExitHooks(s).reload } - def defaults = Command.simple(DefaultsCommand) { (in, s) => + def defaults = Command.command(DefaultsCommand) { s => s ++ DefaultCommands } - def initialize = Command.simple(InitCommand) { (in, s) => + def initialize = Command.command(InitCommand) { s => /*"load-commands -base ~/.sbt/commands" :: */readLines( readable( sbtRCs(s) ) ) ::: s } - def read = Command.simple(ReadCommand, ReadBrief, ReadDetailed) { (in, s) => - getSource(in, s.baseDir) match + def readParser(s: State) = + { + val files = (token(Space) ~> fileParser(s.baseDir)).+ + val portAndSuccess = token(OptSpace) ~> Port + portAndSuccess || files + } + + def read = Command(ReadCommand, ReadBrief, ReadDetailed)(s => readParser(s) map doRead(s)) + + def doRead(s: State)(arg: Either[Int, Seq[File]]): State = + arg match { case Left(portAndSuccess) => val port = math.abs(portAndSuccess) @@ -157,12 +162,6 @@ object Commands s } } - } - private def getSource(in: Input, baseDirectory: File) = - { - try { Left(in.line.stripPrefix(ReadCommand).trim.toInt) } - catch { case _: NumberFormatException => Right(in.splitArgs map { p => new File(baseDirectory, p) }) } - } private def readMessage(port: Int, previousSuccess: Boolean): Option[String] = { // split into two connections because this first connection ends the previous communication @@ -173,15 +172,19 @@ object Commands if(message eq null) None else Some(message) } } - + + // TODO: nest def continuous = - Command( Help(continuousBriefHelp) ) { case (in, s) if in.line startsWith ContinuousExecutePrefix => + Command.single(ContinuousExecutePrefix, Help(continuousBriefHelp) ) { (s, arg) => withAttribute(s, Watch.key, "Continuous execution not configured.") { w => - Watched.executeContinuously(w, s, in) + val repeat = ContinuousExecutePrefix + (if(arg.startsWith(" ")) arg else " " + arg) + Watched.executeContinuously(w, s, arg, repeat) } } - def history = Command( historyHelp: _* ) { case (in, s) if in.line startsWith "!" => + def history = Command.command("!!")(s => s) + //TODO: convert + /*def history = Command( historyHelp: _* ) { case (in, s) if in.line startsWith "!" => val logError = (msg: String) => CommandSupport.logger(s).error(msg) HistoryCommands(in.line.substring(HistoryPrefix.length).trim, (s get HistoryPath.key) getOrElse None, 500/*JLine.MaxHistorySize*/, logError) match { @@ -190,13 +193,13 @@ object Commands (commands ::: s).continue case None => s.fail } - } + }*/ def indent(withStar: Boolean) = if(withStar) "\t*" else "\t " def listProject(name: String, current: Boolean, log: Logger) = log.info( indent(current) + name ) def act = error("TODO") - def projects = Command.simple(ProjectsCommand, projectsBrief, projectsDetailed ) { (in,s) => + def projects = Command.command(ProjectsCommand, projectsBrief, projectsDetailed ) { s => val log = logger(s) val session = Project.session(s) val structure = Project.structure(s) @@ -214,96 +217,36 @@ object Commands case Some(nav) => f(nav) } - def project = Command.simple(ProjectCommand, projectBrief, projectDetailed ) { (in,s) => - val to = in.arguments - val session = Project.session(s) - val structure = Project.structure(s) - val uri = session.currentBuild - def setProject(id: String) = updateCurrent(s.put(SessionKey, session.setCurrent(uri, id))) - if(to.isEmpty) - { - logger(s).info(session.currentProject(uri) + " (in build " + uri + ")") - s - } - else if(to == "/") - { - val id = Load.getRootProject(structure.units)(uri) - setProject(id) - } - else if(to.startsWith("^")) - { - val newBuild = (new java.net.URI(to substring 1)).normalize - if(structure.units contains newBuild) - updateCurrent(s.put(SessionKey, session.setCurrent(uri, session currentProject uri))) - else - { - logger(s).error("Invalid build unit '" + newBuild + "' (type 'projects' to list available builds).") - s - } - } -/* else if(to.forall(_ == '.')) - if(to.length > 1) gotoParent(to.length - 1, nav, s) else s */ // semantics currently undefined - else if( structure.units(uri).defined.contains(to) ) - setProject(to) - else - { - logger(s).error("Invalid project name '" + to + "' (type 'projects' to list available projects).") - s.fail - } - } + def project = Command(ProjectCommand, projectBrief, projectDetailed)(ProjectNavigation.command) - def exit = Command( Help(exitBrief) ) { - case (in, s) if TerminateActions contains in.line => - runExitHooks(s).exit(true) - } + def exit = Command.command(TerminateAction, Help(exitBrief) :: Nil ) ( doExit ) - def discover = Command.simple(Discover, DiscoverBrief, DiscoverDetailed) { (in, s) => + def doExit(s: State): State = runExitHooks(s).exit(true) + + // TODO: tab completion, low priority + def discover = Command.single(Discover, DiscoverBrief, DiscoverDetailed) { (s, arg) => withAttribute(s, Analysis, "No analysis to process.") { analysis => - val command = Parse.discover(in.arguments) + val command = Parse.discover(arg) val discovered = build.Build.discover(analysis, command) println(discovered.mkString("\n")) s } } - def compile = Command.simple(CompileName, CompileBrief, CompileDetailed ) { (in, s) => - val command = Parse.compile(in.arguments)(s.baseDir) + // TODO: tab completion, low priority + def compile = Command.single(CompileName, CompileBrief, CompileDetailed ) { (s, arg) => + val command = Parse.compile(arg)(s.baseDir) try { val analysis = build.Build.compile(command, s.configuration) s.put(Analysis, analysis) } catch { case e: xsbti.CompileFailed => s.fail /* already logged */ } } - def loadProject = Command.simple(LoadProject, LoadProjectBrief, LoadProjectDetailed) { (in, s) => + def loadProject = Command.command(LoadProject, LoadProjectBrief, LoadProjectDetailed) { s => val structure = Load.defaultLoad(s, logger(s)) val session = Load.initialSession(structure) val newAttrs = s.attributes.put(StructureKey, structure).put(SessionKey, session) val newState = s.copy(attributes = newAttrs) - updateCurrent(runExitHooks(newState)) - } - - def updateCurrent(s: State): State = - { - val structure = Project.structure(s) - val (uri, id) = Project.current(s) - val ref = ProjectRef(uri, id) - val project = Load.getProject(structure.units, uri, id) - logger(s).info("Set current project to " + id + " (in build " + uri +")") - - val data = structure.data - val historyPath = HistoryPath(ref).get(data).flatMap(identity) - val newAttrs = s.attributes.put(Watch.key, makeWatched(data, ref, project)).put(HistoryPath.key, historyPath) - s.copy(attributes = newAttrs) - } - def makeWatched(data: Settings[Scope], ref: ProjectRef, project: Project): Watched = - { - def getWatch(ref: ProjectRef) = Watch(ref).get(data) - getWatch(ref) match - { - case Some(currentWatch) => - val subWatches = project.uses flatMap { p => getWatch(p) } - Watched.multi(currentWatch, subWatches) - case None => Watched.empty - } + Project.updateCurrent(runExitHooks(newState)) } def handleException(e: Throwable, s: State, trace: Boolean = true): State = { @@ -318,8 +261,9 @@ object Commands s.copy(exitHooks = Set.empty) } - def loadCommands = Command.simple(LoadCommand, Parse.helpBrief(LoadCommand, LoadCommandLabel), Parse.helpDetail(LoadCommand, LoadCommandLabel, true) ) { (in, s) => - applyCommands(s, buildCommands(in.arguments, s.configuration)) + // TODO: tab completion, low priority + def loadCommands = Command.single(LoadCommand, Parse.helpBrief(LoadCommand, LoadCommandLabel), Parse.helpDetail(LoadCommand, LoadCommandLabel, true) ) { (s, arg) => + applyCommands(s, buildCommands(arg, s.configuration)) } def buildCommands(arguments: String, configuration: xsbti.AppConfiguration): Either[Throwable, Seq[Any]] = @@ -370,33 +314,34 @@ object Commands def addAlias(s: State, name: String, value: String): State = { - val in = Input(name, None) - if(in.name == name) { + if(Command validID name) { val removed = removeAlias(s, name) - if(value.isEmpty) removed else removed.copy(processors = new Alias(name, value) +: removed.processors) + if(value.isEmpty) removed else removed.copy(processors = newAlias(name, value) +: removed.processors) } else { System.err.println("Invalid alias name '" + name + "'.") s.fail } } - def removeAlias(s: State, name: String): State = - s.copy(processors = s.processors.filter { case a: Alias if a.name == name => false; case _ => true } ) + def removeAlias(s: State, name: String): State = s.copy(processors = s.processors.filter(c => !isAliasNamed(name, c)) ) + def isAliasNamed(name: String, c: Command): Boolean = isNamed(name, getAlias(c)) + def isNamed(name: String, alias: Option[(String,String)]): Boolean = alias match { case None => false; case Some((alias,_)) => name != alias } - def printAliases(s: State): Unit = { - val strings = aliasStrings(s) - if(!strings.isEmpty) println( strings.mkString("\t", "\n\t","") ) - } + def getAlias(c: Command): Option[(String,String)] = c.tags get CommandAliasKey + def printAlias(s: State, name: String): Unit = printAliases(aliases(s,(n,v) => n == name) ) + def printAliases(s: State): Unit = printAliases(allAliases(s)) + def printAliases(as: Seq[(String,String)]): Unit = + for( (name,value) <- as) + println("\t" + name + " = " + value) - def printAlias(s: State, name: String): Unit = - for(a <- aliases(s)) if (a.name == name) println("\t" + name + " = " + a.value) + def aliasNames(s: State): Seq[String] = allAliases(s).map(_._1) + def allAliases(s: State): Seq[(String,String)] = aliases(s, (n,v) => true) + def aliases(s: State, pred: (String,String) => Boolean): Seq[(String,String)] = + s.processors.flatMap(c => getAlias(c).filter(tupled(pred))) - def aliasStrings(s: State) = aliases(s).map(a => a.name + " = " + a.value) - def aliases(s: State) = s.processors collect { case a: Alias => a } - - final class Alias(val name: String, val value: String) extends Command { - assert(name.length > 0) - assert(value.length > 0) - def help = _ => Nil - def run = (in, s) => if(in.name == name) Some((value + " " + in.arguments) :: s) else None - } + def newAlias(name: String, value: String): Command = + Command(name, (name, ""), "Alias of '" + value + "'")(aliasBody(name, value)).tag(CommandAliasKey, (name, value)) + def aliasBody(name: String, value: String)(state: State): Parser[State] = + Parser(Command.combine(removeAlias(state,name).processors)(state))(value) + + val CommandAliasKey = AttributeKey[(String,String)]("is-command-alias") } \ No newline at end of file diff --git a/main/Project.scala b/main/Project.scala index 6b1877f30..61951717e 100644 --- a/main/Project.scala +++ b/main/Project.scala @@ -6,6 +6,8 @@ package sbt import java.io.File import java.net.URI import Project._ + import Command.{HistoryPath,Watch} + import CommandSupport.logger final case class Project(id: String, base: File, aggregate: Seq[ProjectRef] = Nil, dependencies: Seq[Project.ClasspathDependency] = Nil, inherits: Seq[ProjectRef] = Nil, settings: Seq[Project.Setting[_]] = Project.defaultSettings, configurations: Seq[Configuration] = Configurations.default) @@ -37,6 +39,31 @@ object Project extends Init[Scope] val (unit, it) = current(state) ProjectRef(Some(unit), Some(it)) } + def updateCurrent(s: State): State = + { + val structure = Project.structure(s) + val (uri, id) = Project.current(s) + val ref = ProjectRef(uri, id) + val project = Load.getProject(structure.units, uri, id) + logger(s).info("Set current project to " + id + " (in build " + uri +")") + + val data = structure.data + val historyPath = HistoryPath(ref).get(data).flatMap(identity) + val newAttrs = s.attributes.put(Watch.key, makeWatched(data, ref, project)).put(HistoryPath.key, historyPath) + s.copy(attributes = newAttrs) + } + + def makeWatched(data: Settings[Scope], ref: ProjectRef, project: Project): Watched = + { + def getWatch(ref: ProjectRef) = Watch(ref).get(data) + getWatch(ref) match + { + case Some(currentWatch) => + val subWatches = project.uses flatMap { p => getWatch(p) } + Watched.multi(currentWatch, subWatches) + case None => Watched.empty + } + } def display(scoped: ScopedKey[_]): String = Scope.display(scoped.scope, scoped.key.label) def mapScope(f: Scope => Scope) = new (ScopedKey ~> ScopedKey) { def apply[T](key: ScopedKey[T]) = diff --git a/main/ProjectNavigation.scala b/main/ProjectNavigation.scala new file mode 100644 index 000000000..e914ede87 --- /dev/null +++ b/main/ProjectNavigation.scala @@ -0,0 +1,77 @@ +/* sbt -- Simple Build Tool + * Copyright 2008, 2009, 2010, 2011 Mark Harrah + */ +package sbt + + import ProjectNavigation._ + import Project.{SessionKey, updateCurrent} + import CommandSupport.logger + import complete.{DefaultParsers, Parser} + import DefaultParsers._ + import java.net.URI + +object ProjectNavigation +{ + sealed trait Navigate + final object ShowCurrent extends Navigate + final object Root extends Navigate + final class ChangeBuild(val base: URI) extends Navigate + final class ChangeProject(val id: String) extends Navigate + + def command(s: State): Parser[State] = + if(s get Project.SessionKey isEmpty) failure("No project loaded") else (new ProjectNavigation(s)).command +} +final class ProjectNavigation(s: State) +{ + val session = Project session s + val structure = Project structure s + val builds = structure.units.keys.toSet + val (uri, pid) = session.current + val projects = Load.getBuild(structure.units, uri).defined.keys + + def setProject(uri: URI, id: String) = updateCurrent(s.put(SessionKey, session.setCurrent(uri, id))) + def getRoot(uri: URI) = Load.getRootProject(structure.units)(uri) + + def apply(action: Navigate): State = + action match + { + case ShowCurrent => show(); s + case Root => setProject(uri, getRoot(uri)) + case b: ChangeBuild => changeBuild(b.base) +/* else if(to.forall(_ == '.')) + if(to.length > 1) gotoParent(to.length - 1, nav, s) else s */ // semantics currently undefined + case s: ChangeProject => selectProject(s.id) + } + + def show(): Unit = logger(s).info(pid + " (in build " + uri + ")") + def selectProject(to: String): State = + if( structure.units(uri).defined.contains(to) ) + setProject(uri, to) + else + fail("Invalid project name '" + to + "' (type 'projects' to list available projects).") + + def changeBuild(to: URI): State = + { + val newBuild = (uri resolve to).normalize + if(structure.units contains newBuild) + setProject(newBuild, getRoot(newBuild)) + else + fail("Invalid build unit '" + newBuild + "' (type 'projects' to list available builds).") + } + def fail(msg: String): State = + { + logger(s).error(msg) + s.fail + } + + import complete.Parser._ + import complete.Parsers._ + + val parser: Parser[Navigate] = + { + val buildP = token('^') ~> token(Uri(builds) map(u => new ChangeBuild(u) ) ) + val projectP = token(ID map (id => new ChangeProject(id)) examples projects.toSet ) + success(ShowCurrent) | ( token(Space) ~> (token('/' ^^^ Root) | buildP | projectP) ) + } + val command: Parser[State] = parser map apply +} \ No newline at end of file diff --git a/main/State.scala b/main/State.scala index 6da718698..1b5b5dac9 100644 --- a/main/State.scala +++ b/main/State.scala @@ -22,6 +22,10 @@ trait Identity { override final def toString = super.toString } +object Next extends Enumeration { + val Reload, Fail, Done, Continue = Value +} + trait StateOps { def process(f: (String, State) => State): State def ::: (commands: Seq[String]): State diff --git a/main/Structure.scala b/main/Structure.scala index 08bbaff54..5d2f7b1ba 100644 --- a/main/Structure.scala +++ b/main/Structure.scala @@ -9,7 +9,7 @@ package sbt import std.TaskExtra._ import Task._ import Project.{ScopedKey, Setting} - import parse.Parser + import complete.Parser import java.io.File import java.net.URI diff --git a/main/Watched.scala b/main/Watched.scala index 95022b10c..803531315 100644 --- a/main/Watched.scala +++ b/main/Watched.scala @@ -29,7 +29,7 @@ object Watched val PollDelaySeconds = 1 def isEnter(key: Int): Boolean = key == 10 || key == 13 - def executeContinuously(watched: Watched, s: State, in: Input): State = + def executeContinuously(watched: Watched, s: State, next: String, repeat: String): State = { @tailrec def shouldTerminate: Boolean = (System.in.available > 0) && (watched.terminateWatch(System.in.read()) || shouldTerminate) val sourcesFinder = watched.watchPaths @@ -41,7 +41,7 @@ object Watched val (triggered, newWatchState) = SourceModificationWatch.watch(sourcesFinder, PollDelaySeconds, watchState)(shouldTerminate) if(triggered) - (in.arguments :: FailureWall :: in.line :: s).put(ContinuousState, newWatchState) + (next :: FailureWall :: repeat :: s).put(ContinuousState, newWatchState) else { while (System.in.available() > 0) System.in.read() diff --git a/util/complete/Completions.scala b/util/complete/Completions.scala index a27e5dc6f..a2b910897 100644 --- a/util/complete/Completions.scala +++ b/util/complete/Completions.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2010 Mark Harrah */ -package sbt.parse +package sbt.complete /** * Represents a set of completions. @@ -72,9 +72,8 @@ sealed trait Completion override final lazy val hashCode = Completion.hashCode(this) override final def equals(o: Any) = o match { case c: Completion => Completion.equal(this, c); case _ => false } } -final class DisplayOnly(display0: String) extends Completion +final class DisplayOnly(val display: String) extends Completion { - lazy val display = display0 def isEmpty = display.isEmpty def append = "" override def toString = "{" + display + "}" diff --git a/util/complete/HistoryCommands.scala b/util/complete/HistoryCommands.scala index bbf8c0bfd..16a359f9a 100644 --- a/util/complete/HistoryCommands.scala +++ b/util/complete/HistoryCommands.scala @@ -83,4 +83,19 @@ object HistoryCommands else history ! s } +/* + import parse.{Parser,Parsers} + import Parser._ + import Parsers._ + val historyParser: Parser[complete.History => Option[String]] = + { + Start ~> Specific) + } + !! Execute the last command again + !: Show all previous commands + !:n Show the last n commands + !n Execute the command with index n, as shown by the !: command + !-n Execute the nth command before this one + !string Execute the most recent command starting with 'string' + !?string*/ } \ No newline at end of file diff --git a/util/complete/JLineCompletion.scala b/util/complete/JLineCompletion.scala index f9c7183ca..9103c3e72 100644 --- a/util/complete/JLineCompletion.scala +++ b/util/complete/JLineCompletion.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2011 Mark Harrah */ -package sbt.parse +package sbt.complete import jline.{CandidateListCompletionHandler,Completor,CompletionHandler,ConsoleReader} import scala.annotation.tailrec @@ -41,14 +41,17 @@ object JLineCompletion customCompletor(str => convertCompletions(Parser.completions(p, str))) def convertCompletions(c: Completions): (Seq[String], Seq[String]) = { - ( (Seq[String](), Seq[String]()) /: c.get) { case ( t @ (insert,display), comp) => - if(comp.isEmpty) t else (insert :+ comp.append, insert :+ comp.display) - } + val (insert, display) = + ( (Set.empty[String], Set.empty[String]) /: c.get) { case ( t @ (insert,display), comp) => + if(comp.isEmpty) t else (insert + comp.append, appendNonEmpty(display, comp.display.trim)) + } + (insert.toSeq, display.toSeq.sorted) } - + def appendNonEmpty(set: Set[String], add: String) = if(add.isEmpty) set else set + add + def customCompletor(f: String => (Seq[String], Seq[String])): ConsoleReader => Boolean = reader => { - val success = complete(beforeCursor(reader), f, reader, false) + val success = complete(beforeCursor(reader), f, reader) reader.flushConsole() success } @@ -59,29 +62,34 @@ object JLineCompletion b.getBuffer.substring(0, b.cursor) } - def complete(beforeCursor: String, completions: String => (Seq[String],Seq[String]), reader: ConsoleReader, inserted: Boolean): Boolean = + // returns false if there was nothing to insert and nothing to display + def complete(beforeCursor: String, completions: String => (Seq[String],Seq[String]), reader: ConsoleReader): Boolean = { val (insert,display) = completions(beforeCursor) - if(insert.isEmpty) - inserted - else - { - lazy val common = commonPrefix(insert) - if(inserted || common.isEmpty) - { - showCompletions(display, reader) - reader.drawLine() - true - } + val common = commonPrefix(insert) + if(common.isEmpty) + if(display.isEmpty) + () else - { - reader.getCursorBuffer.write(common) - reader.redrawLine() - complete(beforeCursor + common, completions, reader, true) - } - } + showCompletions(display, reader) + else + appendCompletion(common, reader) + + !(common.isEmpty && display.isEmpty) } - def showCompletions(cs: Seq[String], reader: ConsoleReader): Unit = + + def appendCompletion(common: String, reader: ConsoleReader) + { + reader.getCursorBuffer.write(common) + reader.redrawLine() + } + + def showCompletions(display: Seq[String], reader: ConsoleReader) + { + printCompletions(display, reader) + reader.drawLine() + } + def printCompletions(cs: Seq[String], reader: ConsoleReader): Unit = if(cs.isEmpty) () else CandidateListCompletionHandler.printCandidates(reader, JavaConversions.asJavaList(cs), true) def commonPrefix(s: Seq[String]): String = if(s.isEmpty) "" else s reduceLeft commonPrefix diff --git a/util/complete/LineReader.scala b/util/complete/LineReader.scala index d586d0729..b8ab32f87 100644 --- a/util/complete/LineReader.scala +++ b/util/complete/LineReader.scala @@ -5,6 +5,7 @@ package sbt import jline.{Completor, ConsoleReader} import java.io.File + import complete.Parser abstract class JLine extends LineReader { @@ -56,27 +57,21 @@ private object JLine val MaxHistorySize = 500 } -trait LineReader extends NotNull +trait LineReader { def readLine(prompt: String): Option[String] } -private[sbt] final class LazyJLineReader(historyPath: Option[File] /*, completor: => Completor*/) extends JLine +final class FullReader(historyPath: Option[File], complete: Parser[_]) extends JLine { protected[this] val reader = { val cr = new ConsoleReader cr.setBellEnabled(false) JLine.initializeHistory(cr, historyPath) -// cr.addCompletor(new LazyCompletor(completor)) + sbt.complete.JLineCompletion.installCustomCompletor(cr, complete) cr } } -private class LazyCompletor(delegate0: => Completor) extends Completor -{ - private lazy val delegate = delegate0 - def complete(buffer: String, cursor: Int, candidates: java.util.List[_]): Int = - delegate.complete(buffer, cursor, candidates) -} class SimpleReader private[sbt] (historyPath: Option[File]) extends JLine { diff --git a/util/complete/Parser.scala b/util/complete/Parser.scala index 4944c6d77..cb94bee85 100644 --- a/util/complete/Parser.scala +++ b/util/complete/Parser.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool - * Copyright 2008, 2010 Mark Harrah + * Copyright 2008, 2010, 2011 Mark Harrah */ -package sbt.parse +package sbt.complete import Parser._ @@ -11,7 +11,7 @@ sealed trait Parser[+T] def resultEmpty: Option[T] def result: Option[T] = None def completions: Completions - def valid: Boolean = true + def valid: Boolean def isTokenStart = false } sealed trait RichParser[A] @@ -54,56 +54,8 @@ sealed trait RichParser[A] def flatMap[B](f: A => Parser[B]): Parser[B] } -object Parser +object Parser extends ParserMain { - def apply[T](p: Parser[T])(s: String): Parser[T] = - (p /: s)(derive1) - - def derive1[T](p: Parser[T], c: Char): Parser[T] = - if(p.valid) p.derive(c) else p - - def completions(p: Parser[_], s: String): Completions = completions( apply(p)(s) ) - def completions(p: Parser[_]): Completions = p.completions - - implicit def richParser[A](a: Parser[A]): RichParser[A] = new RichParser[A] - { - def ~[B](b: Parser[B]) = seqParser(a, b) - def ||[B](b: Parser[B]) = choiceParser(a,b) - def |[B >: A](b: Parser[B]) = homParser(a,b) - def ? = opt(a) - def * = zeroOrMore(a) - def + = oneOrMore(a) - def map[B](f: A => B) = mapParser(a, f) - def id = a - - def ^^^[B](value: B): Parser[B] = a map { _ => value } - def ??[B >: A](alt: B): Parser[B] = a.? map { _ getOrElse alt } - def <~[B](b: Parser[B]): Parser[A] = (a ~ b) map { case av ~ _ => av } - def ~>[B](b: Parser[B]): Parser[B] = (a ~ b) map { case _ ~ bv => bv } - - def unary_- = not(a) - def & (o: Parser[_]) = and(a, o) - def - (o: Parser[_]) = sub(a, o) - def examples(s: String*): Parser[A] = examples(s.toSet) - def examples(s: Set[String]): Parser[A] = Parser.examples(a, s, check = true) - def filter(f: A => Boolean): Parser[A] = filterParser(a, f) - def string(implicit ev: A <:< Seq[Char]): Parser[String] = map(_.mkString) - def flatMap[B](f: A => Parser[B]) = bindParser(a, f) - } - implicit def literalRichParser(c: Char): RichParser[Char] = richParser(c) - implicit def literalRichParser(s: String): RichParser[String] = richParser(s) - - def examples[A](a: Parser[A], completions: Set[String], check: Boolean = false): Parser[A] = - if(a.valid) { - a.result match - { - case Some(av) => success( av ) - case None => - if(check) checkMatches(a, completions.toSeq) - new Examples(a, completions) - } - } - else Invalid def checkMatches(a: Parser[_], completions: Seq[String]) { @@ -135,37 +87,22 @@ object Parser if(a.valid) { a.result match { - case Some(av) => if( f(av) ) success( av ) else Invalid + case Some(av) => if( f(av) ) successStrict( av ) else Invalid case None => new Filter(a, f) } } else Invalid def seqParser[A,B](a: Parser[A], b: Parser[B]): Parser[(A,B)] = - if(a.valid && b.valid) { + if(a.valid && b.valid) (a.result, b.result) match { - case (Some(av), Some(bv)) => success( (av, bv) ) + case (Some(av), Some(bv)) => successStrict( (av, bv) ) case (Some(av), None) => b map { bv => (av, bv) } case (None, Some(bv)) => a map { av => (av, bv) } case (None, None) => new SeqParser(a,b) } - } else Invalid - def token[T](t: Parser[T]): Parser[T] = token(t, "", true) - def token[T](t: Parser[T], description: String): Parser[T] = token(t, description, false) - def token[T](t: Parser[T], seen: String, track: Boolean): Parser[T] = - if(t.valid && !t.isTokenStart) - if(t.result.isEmpty) new TokenStart(t, seen, track) else t - else - t - - def homParser[A](a: Parser[A], b: Parser[A]): Parser[A] = - if(a.valid) - if(b.valid) new HomParser(a, b) else a - else - b - def choiceParser[A,B](a: Parser[A], b: Parser[B]): Parser[Either[A,B]] = if(a.valid) if(b.valid) new HetParser(a,b) else a.map( Left(_) ) @@ -173,14 +110,14 @@ object Parser b.map( Right(_) ) def opt[T](a: Parser[T]): Parser[Option[T]] = - if(a.valid) new Optional(a) else success(None) + if(a.valid) new Optional(a) else successStrict(None) def zeroOrMore[T](p: Parser[T]): Parser[Seq[T]] = repeat(p, 0, Infinite) def oneOrMore[T](p: Parser[T]): Parser[Seq[T]] = repeat(p, 1, Infinite) def repeat[T](p: Parser[T], min: Int = 0, max: UpperBound = Infinite): Parser[Seq[T]] = repeat(None, p, min, max, Nil) - private[parse] def repeat[T](partial: Option[Parser[T]], repeated: Parser[T], min: Int, max: UpperBound, revAcc: List[T]): Parser[Seq[T]] = + private[complete] def repeat[T](partial: Option[Parser[T]], repeated: Parser[T], min: Int, max: UpperBound, revAcc: List[T]): Parser[Seq[T]] = { assume(min >= 0, "Minimum must be greater than or equal to zero (was " + min + ")") assume(max >= min, "Minimum must be less than or equal to maximum (min: " + min + ", max: " + max + ")") @@ -189,8 +126,8 @@ object Parser if(repeated.valid) repeated.result match { - case Some(value) => success(revAcc reverse_::: value :: Nil) // revAcc should be Nil here - case None => if(max.isZero) success(revAcc.reverse) else new Repeat(partial, repeated, min, max, revAcc) + case Some(value) => successStrict(revAcc reverse_::: value :: Nil) // revAcc should be Nil here + case None => if(max.isZero) successStrict(revAcc.reverse) else new Repeat(partial, repeated, min, max, revAcc) } else if(min == 0) invalidButOptional @@ -207,27 +144,55 @@ object Parser case None => checkRepeated(part.map(lv => (lv :: revAcc).reverse)) } else Invalid - case None => checkRepeated(success(Nil)) + case None => checkRepeated(successStrict(Nil)) } } - def success[T](value: T): Parser[T] = new Parser[T] { - override def result = Some(value) + def sub[T](a: Parser[T], b: Parser[_]): Parser[T] = and(a, not(b)) + + def and[T](a: Parser[T], b: Parser[_]): Parser[T] = if(a.valid && b.valid) new And(a, b) else Invalid +} +trait ParserMain +{ + implicit def richParser[A](a: Parser[A]): RichParser[A] = new RichParser[A] + { + def ~[B](b: Parser[B]) = seqParser(a, b) + def ||[B](b: Parser[B]) = choiceParser(a,b) + def |[B >: A](b: Parser[B]) = homParser(a,b) + def ? = opt(a) + def * = zeroOrMore(a) + def + = oneOrMore(a) + def map[B](f: A => B) = mapParser(a, f) + def id = a + + def ^^^[B](value: B): Parser[B] = a map { _ => value } + def ??[B >: A](alt: B): Parser[B] = a.? map { _ getOrElse alt } + def <~[B](b: Parser[B]): Parser[A] = (a ~ b) map { case av ~ _ => av } + def ~>[B](b: Parser[B]): Parser[B] = (a ~ b) map { case _ ~ bv => bv } + + def unary_- = not(a) + def & (o: Parser[_]) = and(a, o) + def - (o: Parser[_]) = sub(a, o) + def examples(s: String*): Parser[A] = examples(s.toSet) + def examples(s: Set[String]): Parser[A] = Parser.examples(a, s, check = true) + def filter(f: A => Boolean): Parser[A] = filterParser(a, f) + def string(implicit ev: A <:< Seq[Char]): Parser[String] = map(_.mkString) + def flatMap[B](f: A => Parser[B]) = bindParser(a, f) + } + implicit def literalRichParser(c: Char): RichParser[Char] = richParser(c) + implicit def literalRichParser(s: String): RichParser[String] = richParser(s) + + def failure[T](msg: String): Parser[T] = Invalid(msg) + def successStrict[T](value: T): Parser[T] = success(value) + def success[T](value: => T): Parser[T] = new ValidParser[T] { + private[this] lazy val v = value + override def result = Some(v) def resultEmpty = result def derive(c: Char) = Invalid def completions = Completions.empty - override def toString = "success(" + value + ")" + override def toString = "success(" + v + ")" } - val any: Parser[Char] = charClass(_ => true) - - def sub[T](a: Parser[T], b: Parser[_]): Parser[T] = and(a, not(b)) - - def and[T](a: Parser[T], b: Parser[_]): Parser[T] = - if(a.valid && b.valid) new And(a, b) else Invalid - - def not(p: Parser[_]): Parser[Unit] = new Not(p) - implicit def range(r: collection.immutable.NumericRange[Char]): Parser[Char] = new CharacterClass(r contains _).examples(r.map(_.toString) : _*) def chars(legal: String): Parser[Char] = @@ -237,29 +202,97 @@ object Parser } def charClass(f: Char => Boolean): Parser[Char] = new CharacterClass(f) - implicit def literal(ch: Char): Parser[Char] = new Parser[Char] { + implicit def literal(ch: Char): Parser[Char] = new ValidParser[Char] { def resultEmpty = None - def derive(c: Char) = if(c == ch) success(ch) else Invalid + def derive(c: Char) = if(c == ch) successStrict(ch) else Invalid def completions = Completions.single(Completion.suggestStrict(ch.toString)) override def toString = "'" + ch + "'" } implicit def literal(s: String): Parser[String] = stringLiteral(s, s.toList) - def stringLiteral(s: String, remaining: List[Char]): Parser[String] = - if(s.isEmpty) error("String literal cannot be empty") else if(remaining.isEmpty) success(s) else new StringLiteral(s, remaining) - object ~ { def unapply[A,B](t: (A,B)): Some[(A,B)] = Some(t) } + + // intended to be temporary pending proper error feedback + def result[T](p: Parser[T], s: String): Either[(String,Int), T] = + { + /* def loop(i: Int, a: Parser[T]): Either[(String,Int), T] = + a.err match + { + case Some(msg) => Left((msg, i)) + case None => + val ci = i+1 + if(ci >= s.length) + a.resultEmpty.toRight(("", i)) + else + loop(ci, a derive s(ci)) + } + loop(-1, p)*/ + apply(p)(s).resultEmpty.toRight(("Parse error", 0)) + } + + def apply[T](p: Parser[T])(s: String): Parser[T] = + (p /: s)(derive1) + + def derive1[T](p: Parser[T], c: Char): Parser[T] = + if(p.valid) p.derive(c) else p + + // The x Completions.empty removes any trailing token completions where append.isEmpty + def completions(p: Parser[_], s: String): Completions = apply(p)(s).completions x Completions.empty + + def examples[A](a: Parser[A], completions: Set[String], check: Boolean = false): Parser[A] = + if(a.valid) { + a.result match + { + case Some(av) => successStrict( av ) + case None => + if(check) checkMatches(a, completions.toSeq) + new Examples(a, completions) + } + } + else a + + def matched(t: Parser[_], seenReverse: List[Char] = Nil, partial: Boolean = false): Parser[String] = + if(!t.valid) + if(partial && !seenReverse.isEmpty) successStrict(seenReverse.reverse.mkString) else Invalid + else if(t.result.isEmpty) + new MatchedString(t, seenReverse, partial) + else + successStrict(seenReverse.reverse.mkString) + + def token[T](t: Parser[T]): Parser[T] = token(t, "", true) + def token[T](t: Parser[T], description: String): Parser[T] = token(t, description, false) + def token[T](t: Parser[T], seen: String, track: Boolean): Parser[T] = + if(t.valid && !t.isTokenStart) + if(t.result.isEmpty) new TokenStart(t, seen, track) else t + else + t + + def homParser[A](a: Parser[A], b: Parser[A]): Parser[A] = + if(a.valid) + if(b.valid) new HomParser(a, b) else a + else + b + + def not(p: Parser[_]): Parser[Unit] = new Not(p) + + def stringLiteral(s: String, remaining: List[Char]): Parser[String] = + if(s.isEmpty) error("String literal cannot be empty") else if(remaining.isEmpty) success(s) else new StringLiteral(s, remaining) } -private final object Invalid extends Parser[Nothing] +sealed trait ValidParser[T] extends Parser[T] +{ + final def valid = true +} +private object Invalid extends Invalid("inv") +private sealed case class Invalid(val message: String) extends Parser[Nothing] { def resultEmpty = None def derive(c: Char) = error("Invalid.") override def valid = false def completions = Completions.nil - override def toString = "inv" + override def toString = message } -private final class SeqParser[A,B](a: Parser[A], b: Parser[B]) extends Parser[(A,B)] +private final class SeqParser[A,B](a: Parser[A], b: Parser[B]) extends ValidParser[(A,B)] { def cross(ao: Option[A], bo: Option[B]): Option[(A,B)] = for(av <- ao; bv <- bo) yield (av,bv) lazy val resultEmpty = cross(a.resultEmpty, b.resultEmpty) @@ -276,44 +309,44 @@ private final class SeqParser[A,B](a: Parser[A], b: Parser[B]) extends Parser[(A override def toString = "(" + a + " ~ " + b + ")" } -private final class HomParser[A](a: Parser[A], b: Parser[A]) extends Parser[A] +private final class HomParser[A](a: Parser[A], b: Parser[A]) extends ValidParser[A] { def derive(c: Char) = (a derive c) | (b derive c) lazy val resultEmpty = a.resultEmpty orElse b.resultEmpty lazy val completions = a.completions ++ b.completions override def toString = "(" + a + " | " + b + ")" } -private final class HetParser[A,B](a: Parser[A], b: Parser[B]) extends Parser[Either[A,B]] +private final class HetParser[A,B](a: Parser[A], b: Parser[B]) extends ValidParser[Either[A,B]] { def derive(c: Char) = (a derive c) || (b derive c) lazy val resultEmpty = a.resultEmpty.map(Left(_)) orElse b.resultEmpty.map(Right(_)) lazy val completions = a.completions ++ b.completions override def toString = "(" + a + " || " + b + ")" } -private final class BindParser[A,B](a: Parser[A], f: A => Parser[B]) extends Parser[B] +private final class BindParser[A,B](a: Parser[A], f: A => Parser[B]) extends ValidParser[B] { lazy val resultEmpty = a.resultEmpty match { case None => None; case Some(av) => f(av).resultEmpty } - lazy val completions = { + lazy val completions = a.completions flatMap { c => apply(a)(c.append).resultEmpty match { case None => Completions.strict(Set.empty + c) case Some(av) => c x f(av).completions } } - } def derive(c: Char) = { val common = a derive c flatMap f a.resultEmpty match { - case Some(av) => common | f(av).derive(c) + case Some(av) => common | derive1(f(av), c) case None => common } } + override def isTokenStart = a.isTokenStart override def toString = "bind(" + a + ")" } -private final class MapParser[A,B](a: Parser[A], f: A => B) extends Parser[B] +private final class MapParser[A,B](a: Parser[A], f: A => B) extends ValidParser[B] { lazy val resultEmpty = a.resultEmpty map f def derive(c: Char) = (a derive c) map f @@ -321,14 +354,24 @@ private final class MapParser[A,B](a: Parser[A], f: A => B) extends Parser[B] override def isTokenStart = a.isTokenStart override def toString = "map(" + a + ")" } -private final class Filter[T](p: Parser[T], f: T => Boolean) extends Parser[T] +private final class Filter[T](p: Parser[T], f: T => Boolean) extends ValidParser[T] { lazy val resultEmpty = p.resultEmpty filter f def derive(c: Char) = (p derive c) filter f lazy val completions = p.completions filterS { s => apply(p)(s).resultEmpty.filter(f).isDefined } override def toString = "filter(" + p + ")" + override def isTokenStart = p.isTokenStart } -private final class TokenStart[T](delegate: Parser[T], seen: String, track: Boolean) extends Parser[T] +private final class MatchedString(delegate: Parser[_], seenReverse: List[Char], partial: Boolean) extends ValidParser[String] +{ + lazy val seen = seenReverse.reverse.mkString + def derive(c: Char) = matched(delegate derive c, c :: seenReverse, partial) + def completions = delegate.completions + def resultEmpty = if(delegate.resultEmpty.isDefined) Some(seen) else if(partial) Some(seen) else None + override def isTokenStart = delegate.isTokenStart + override def toString = "matched(" + partial + ", " + seen + ", " + delegate + ")" +} +private final class TokenStart[T](delegate: Parser[T], seen: String, track: Boolean) extends ValidParser[T] { def derive(c: Char) = token( delegate derive c, if(track) seen + c else seen, track) lazy val completions = @@ -344,27 +387,31 @@ private final class TokenStart[T](delegate: Parser[T], seen: String, track: Bool override def isTokenStart = true override def toString = "token('" + seen + "', " + track + ", " + delegate + ")" } -private final class And[T](a: Parser[T], b: Parser[_]) extends Parser[T] +private final class And[T](a: Parser[T], b: Parser[_]) extends ValidParser[T] { def derive(c: Char) = (a derive c) & (b derive c) lazy val completions = a.completions.filterS(s => apply(b)(s).resultEmpty.isDefined ) lazy val resultEmpty = if(b.resultEmpty.isDefined) a.resultEmpty else None } -private final class Not(delegate: Parser[_]) extends Parser[Unit] +private final class Not(delegate: Parser[_]) extends ValidParser[Unit] { def derive(c: Char) = if(delegate.valid) not(delegate derive c) else this def completions = Completions.empty lazy val resultEmpty = if(delegate.resultEmpty.isDefined) None else Some(()) } -private final class Examples[T](delegate: Parser[T], fixed: Set[String]) extends Parser[T] +private final class Examples[T](delegate: Parser[T], fixed: Set[String]) extends ValidParser[T] { - def derive(c: Char) = examples(delegate derive c, fixed.collect { case x if x.length > 0 && x(0) == c => x.tail }) - def resultEmpty = delegate.resultEmpty - lazy val completions = if(fixed.isEmpty) Completions.empty else Completions(fixed map(f => Completion.suggestion(f)) ) + def derive(c: Char) = examples(delegate derive c, fixed.collect { case x if x.length > 0 && x(0) == c => x substring 1 }) + lazy val resultEmpty = delegate.resultEmpty + lazy val completions = + if(fixed.isEmpty) + if(resultEmpty.isEmpty) Completions.nil else Completions.empty + else + Completions(fixed map(f => Completion.suggestion(f)) ) override def toString = "examples(" + delegate + ", " + fixed.take(2) + ")" } -private final class StringLiteral(str: String, remaining: List[Char]) extends Parser[String] +private final class StringLiteral(str: String, remaining: List[Char]) extends ValidParser[String] { assert(str.length > 0 && !remaining.isEmpty) def resultEmpty = None @@ -372,21 +419,21 @@ private final class StringLiteral(str: String, remaining: List[Char]) extends Pa lazy val completions = Completions.single(Completion.suggestion(remaining.mkString)) override def toString = '"' + str + '"' } -private final class CharacterClass(f: Char => Boolean) extends Parser[Char] +private final class CharacterClass(f: Char => Boolean) extends ValidParser[Char] { def resultEmpty = None - def derive(c: Char) = if( f(c) ) success(c) else Invalid + def derive(c: Char) = if( f(c) ) successStrict(c) else Invalid def completions = Completions.empty override def toString = "class()" } -private final class Optional[T](delegate: Parser[T]) extends Parser[Option[T]] +private final class Optional[T](delegate: Parser[T]) extends ValidParser[Option[T]] { def resultEmpty = Some(None) def derive(c: Char) = (delegate derive c).map(Some(_)) lazy val completions = Completion.empty +: delegate.completions override def toString = delegate.toString + "?" } -private final class Repeat[T](partial: Option[Parser[T]], repeated: Parser[T], min: Int, max: UpperBound, accumulatedReverse: List[T]) extends Parser[Seq[T]] +private final class Repeat[T](partial: Option[Parser[T]], repeated: Parser[T], min: Int, max: UpperBound, accumulatedReverse: List[T]) extends ValidParser[Seq[T]] { assume(0 <= min, "Minimum occurences must be non-negative") assume(max >= min, "Minimum occurences must be less than the maximum occurences") diff --git a/util/complete/Parsers.scala b/util/complete/Parsers.scala new file mode 100644 index 000000000..53d47e27d --- /dev/null +++ b/util/complete/Parsers.scala @@ -0,0 +1,53 @@ +/* sbt -- Simple Build Tool + * Copyright 2011 Mark Harrah + */ +package sbt.complete + + import Parser._ + import java.io.File + import java.net.URI + import java.lang.Character.{getType, MATH_SYMBOL, OTHER_SYMBOL, DASH_PUNCTUATION, OTHER_PUNCTUATION, MODIFIER_SYMBOL, CURRENCY_SYMBOL} + +// Some predefined parsers +trait Parsers +{ + lazy val any: Parser[Char] = charClass(_ => true) + + lazy val DigitSet = Set("0","1","2","3","4","5","6","7","8","9") + lazy val Digit = charClass(_.isDigit) examples DigitSet + lazy val Letter = charClass(_.isLetter) + def IDStart = Letter + lazy val IDChar = charClass(isIDChar) + lazy val ID = IDStart ~ IDChar.* map { case x ~ xs => (x +: xs).mkString } + lazy val OpChar = charClass(isOpChar) + lazy val Op = OpChar.+.string + lazy val OpOrID = ID | Op + + def isOpChar(c: Char) = !isDelimiter(c) && isOpType(getType(c)) + def isOpType(cat: Int) = cat match { case MATH_SYMBOL | OTHER_SYMBOL | DASH_PUNCTUATION | OTHER_PUNCTUATION | MODIFIER_SYMBOL | CURRENCY_SYMBOL => true; case _ => false } + def isIDChar(c: Char) = c.isLetterOrDigit || c == '-' || c == '_' + def isDelimiter(c: Char) = c match { case '`' | '\'' | '\"' | /*';' | */',' | '.' => true ; case _ => false } + + lazy val NotSpaceClass = charClass(!_.isWhitespace) + lazy val SpaceClass = charClass(_.isWhitespace) + lazy val NotSpace = NotSpaceClass.+.string + lazy val Space = SpaceClass.+.examples(" ") + lazy val OptSpace = SpaceClass.*.examples(" ") + + // TODO: implement + def fileParser(base: File): Parser[File] = token(mapOrFail(NotSpace)(s => new File(s.mkString)), "") + + lazy val Port = token(IntBasic, "") + lazy val IntBasic = mapOrFail( '-'.? ~ Digit.+ )( Function.tupled(toInt) ) + private[this] def toInt(neg: Option[Char], digits: Seq[Char]): Int = + (neg.toSeq ++ digits).mkString.toInt + + def mapOrFail[S,T](p: Parser[S])(f: S => T): Parser[T] = + p flatMap { s => try { successStrict(f(s)) } catch { case e: Exception => failure(e.toString) } } + + def spaceDelimited(display: String): Parser[Seq[String]] = (token(Space) ~> token(NotSpace, display)).* + + def Uri(ex: Set[URI]) = NotSpace map { uri => new URI(uri) } examples(ex.map(_.toString)) +} +object Parsers extends Parsers +object DefaultParsers extends Parsers with ParserMain \ No newline at end of file diff --git a/util/complete/UpperBound.scala b/util/complete/UpperBound.scala index 9427070f7..ba1a69ef9 100644 --- a/util/complete/UpperBound.scala +++ b/util/complete/UpperBound.scala @@ -1,7 +1,7 @@ /* sbt -- Simple Build Tool * Copyright 2008,2010 Mark Harrah */ -package sbt.parse +package sbt.complete sealed trait UpperBound { diff --git a/util/complete/src/test/scala/ParserTest.scala b/util/complete/src/test/scala/ParserTest.scala index 257171016..244982f43 100644 --- a/util/complete/src/test/scala/ParserTest.scala +++ b/util/complete/src/test/scala/ParserTest.scala @@ -1,4 +1,4 @@ -package sbt.parse +package sbt.complete import Parser._ import org.scalacheck._ @@ -25,14 +25,14 @@ object JLineTest } object ParserTest extends Properties("Completing Parser") { - val wsc = charClass(_.isWhitespace) - val ws = ( wsc + ) examples(" ") - val optWs = ( wsc * ) examples("") + import Parsers._ val nested = (token("a1") ~ token("b2")) ~ "c3" val nestedDisplay = (token("a1", "") ~ token("b2", "")) ~ "c3" - def p[T](f: T): T = { /*println(f);*/ f } + val spacePort = (token(Space) ~> Port) + + def p[T](f: T): T = { println(f); f } def checkSingle(in: String, expect: Completion)(expectDisplay: Completion = expect) = ( ("token '" + in + "'") |: checkOne(in, nested, expect)) && @@ -56,6 +56,13 @@ object ParserTest extends Properties("Completing Parser") property("nested tokens c") = checkSingle("a1b2", Completion.suggestStrict("c3") )() property("nested tokens c3") = checkSingle("a1b2c", Completion.suggestStrict("3"))() property("nested tokens c inv") = checkInvalid("a1b2a") + + property("suggest space") = checkOne("", spacePort, Completion.tokenStrict("", " ")) + property("suggest port") = checkOne(" ", spacePort, Completion.displayStrict("") ) + property("no suggest at end") = checkOne("asdf", "asdf", Completion.suggestStrict("")) + property("no suggest at token end") = checkOne("asdf", token("asdf"), Completion.suggestStrict("")) + property("empty suggest for examples") = checkOne("asdf", any.+.examples("asdf", "qwer"), Completion.suggestStrict("")) + property("empty suggest for examples token") = checkOne("asdf", token(any.+.examples("asdf", "qwer")), Completion.suggestStrict("")) } object ParserExample {