diff --git a/main/Command.scala b/main/Command.scala index 1b3cb3c26..ff81d0b21 100644 --- a/main/Command.scala +++ b/main/Command.scala @@ -142,6 +142,13 @@ object Command ( (c & opOrIDSpaced(name)) ~ c.+) map { case (f, rem) => (f +: rem).mkString } } +sealed trait InspectOption +object InspectOption +{ + final case class Details(actual: Boolean) extends InspectOption + case object DependencyTree extends InspectOption +} + trait Help { def detail: Map[String, String] diff --git a/main/CommandSupport.scala b/main/CommandSupport.scala index 881c9cdec..05c1e2225 100644 --- a/main/CommandSupport.scala +++ b/main/CommandSupport.scala @@ -84,14 +84,17 @@ LastCommand + """ See also '""" + LastGrepCommand + "'." val InspectCommand = "inspect" - val inspectBrief = (InspectCommand + " ", "Prints the value for 'key', the defining scope, delegates, related definitions, and dependencies.") + val inspectBrief = (InspectCommand + " [tree] ", "Prints the value for 'key', the defining scope, delegates, related definitions, and dependencies.") val inspectDetailed = -InspectCommand + """ +InspectCommand + """ [tree] For a plain setting, the value bound to the key argument is displayed using its toString method. Otherwise, the type of task ("Task" or "Input task") is displayed. "Dependencies" shows the settings that this setting depends on. + If 'tree' is specified, the bound value as well as the settings that this setting depends on + (and their bound values) are displayed as a tree structure. + "Reverse dependencies" shows the settings that depend on this setting. When a key is resolved to a value, it may not actually be defined in the requested scope. diff --git a/main/Main.scala b/main/Main.scala index f1953f636..033b398f1 100644 --- a/main/Main.scala +++ b/main/Main.scala @@ -382,10 +382,16 @@ object BuiltinCommands val newSession = session.appendSettings( append map (a => (a, arg))) reapply(newSession, structure, s) } - def inspect = Command(InspectCommand, inspectBrief, inspectDetailed)(inspectParser) { case (s,(actual,sk)) => - val detailString = Project.details(Project.structure(s), actual, sk.scope, sk.key)( Project.showContextKey(s) ) - logger(s).info(detailString) - s + def inspect = Command(InspectCommand, inspectBrief, inspectDetailed)(inspectParser) { + case (s, (InspectOption.DependencyTree, sk)) => + val basedir = new File(Project.session(s).current.build) + val treeString = Project.settingGraph(Project.structure(s), basedir, sk)( Project.showContextKey(s) ).dependsAscii + logger(s).info(treeString) + s + case (s, (InspectOption.Details(actual), sk)) => + val detailString = Project.details(Project.structure(s), actual, sk.scope, sk.key)( Project.showContextKey(s) ) + logger(s).info(detailString) + s } def lastGrep = Command(LastGrepCommand, lastGrepBrief, lastGrepDetailed)(lastGrepParser) { case (s, (pattern,Some(sk))) => @@ -401,7 +407,13 @@ object BuiltinCommands val ext = Project.extract(s) (ext.structure, Select(ext.currentRef), ext.showKey) } - def inspectParser = (s: State) => token((Space ~> ("actual" ^^^ true)) ?? false) ~ spacedKeyParser(s) + def inspectParser = (s: State) => spacedInspectOptionParser(s) ~ spacedKeyParser(s) + val spacedInspectOptionParser: (State => Parser[InspectOption]) = (s: State) => { + import InspectOption._ + val actual = "actual" ^^^ Details(true) + val tree = "tree" ^^^ DependencyTree + token(Space ~> (tree | actual)) ?? Details(false) + } val spacedKeyParser = (s: State) => Act.requireSession(s, token(Space) ~> Act.scopedKeyParser(s)) val optSpacedKeyParser = (s: State) => spacedKeyParser(s).? def lastGrepParser(s: State) = Act.requireSession(s, (token(Space) ~> token(NotSpace, "")) ~ optSpacedKeyParser(s)) diff --git a/main/Project.scala b/main/Project.scala index 4cbb2685f..eb610d046 100644 --- a/main/Project.scala +++ b/main/Project.scala @@ -291,6 +291,8 @@ object Project extends Init[Scope] with ProjectExtra printScopes("Delegates", delegates(structure, scope, key)) + printScopes("Related", related) } + def settingGraph(structure: BuildStructure, basedir: File, scoped: ScopedKey[_])(implicit display: Show[ScopedKey[_]]): SettingGraph = + SettingGraph(structure, basedir, scoped, 0) def graphSettings(structure: BuildStructure, basedir: File)(implicit display: Show[ScopedKey[_]]) { def graph(actual: Boolean, name: String) = graphSettings(structure, actual, name, new File(basedir, name + ".dot")) diff --git a/main/SettingGraph.scala b/main/SettingGraph.scala new file mode 100644 index 000000000..d133a73ba --- /dev/null +++ b/main/SettingGraph.scala @@ -0,0 +1,96 @@ +/* sbt -- Simple Build Tool + * Copyright 2011 Mark Harrah, Eugene Yokota + */ +package sbt + + import java.net.URI + import java.io.File + import Project.{ScopedKey, flattenLocals, compiled} + import Load.BuildStructure + import Predef.{any2stringadd => _, _} + +object SettingGraph +{ + def apply(structure: BuildStructure, basedir: File, scoped: ScopedKey[_], generation: Int) + (implicit display: Show[ScopedKey[_]]): SettingGraph = + { + val key = scoped.key + val scope = scoped.scope + + lazy val clazz = key.manifest.erasure + lazy val firstType = key.manifest.typeArguments.head + val (typeName: String, value: Option[_]) = + structure.data.get(scope, key) match { + case None => ("", None) + case Some(v) => + if(clazz == classOf[Task[_]]) ("Task[" + firstType.toString + "]", None) + else if(clazz == classOf[InputTask[_]]) ("InputTask[" + firstType.toString + "]", None) + else (key.manifest.toString, Some(v)) + } + + val definedIn = structure.data.definingScope(scope, key) map { sc => display(ScopedKey(sc, key)) } + val cMap = flattenLocals(compiled(structure.settings, false)(structure.delegates, structure.scopeLocal, display)) + // val related = cMap.keys.filter(k => k.key == key && k.scope != scope) + val depends = cMap.get(scoped) match { case Some(c) => c.dependencies.toSet; case None => Set.empty } + // val reverse = reverseDependencies(cMap, scoped) + + SettingGraph(display(scoped), definedIn, typeName, value, key.description, basedir, + depends map { apply(structure, basedir, _, generation + 1) }) + } +} + +case class SettingGraph(name: String, + definedIn: Option[String], + typeName: String, + value: Option[Any], + description: Option[String], + basedir: File, + depends: Set[SettingGraph]) +{ + def valueString: String = + value map { + case f: File => IO.relativize(basedir, f) getOrElse {f.toString} + case x => x.toString + } getOrElse {typeName} + + def dependsAscii: String = Graph.toAscii(this, + (x: SettingGraph) => x.depends.toSeq, + (x: SettingGraph) => "%s = %s" format(x.definedIn getOrElse {""}, x.valueString)) +} + +object Graph +{ + // [info] foo + // [info] +-bar + // [info] | +-baz + // [info] | + // [info] +-quux + def toAscii[A](top: A, children: A => Seq[A], display: A => String): String = { + val maxColumn = jline.Terminal.getTerminal.getTerminalWidth - 8 + val twoSpaces = " " + " " // prevent accidentally being converted into a tab + def limitLine(s: String): String = + if (s.length > maxColumn) s.slice(0, maxColumn - 2) + ".." + else s + def insertBar(s: String, at: Int): String = + s.slice(0, at) + + (s(at).toString match { + case " " => "|" + case x => x + }) + + s.slice(at + 1, s.length) + def toAsciiLines(node: A, level: Int): Vector[String] = { + val line = limitLine((twoSpaces * level) + (if (level == 0) "" else "+-") + display(node)) + val cs = Vector(children(node): _*) + val childLines = cs map {toAsciiLines(_, level + 1)} + val withBar = childLines.zipWithIndex flatMap { + case (lines, pos) if pos < (cs.size - 1) => lines map {insertBar(_, 2 * (level + 1))} + case (lines, pos) => + if (lines.last.trim != "") lines ++ Vector(twoSpaces * (level + 1)) + else lines + } + line +: withBar + } + + toAsciiLines(top, 0).mkString("\n") + } +}