Permalink
Browse files

Initial Sbt integration: Add 'Show Sbt Console' in the Project menu,

allow building from the Sbt console. Hyperlink form error trees to
sources.
  • Loading branch information...
dragos committed Apr 19, 2011
1 parent 400c8a1 commit d880092e0cc48c8be1933fb968931baf70383d9b
@@ -196,6 +196,15 @@
tooltip="Report a bug in the Scala Development Tools"
class="scala.tools.eclipse.actions.RunDiagnosticAction"
enablesFor="*">
+ </action>
+ <action
+ class="scala.tools.eclipse.sbt.ShowSbtConsoleAction"
+ enablesFor="*"
+ id="org.scala-ide.sdt.ui.runSbt"
+ label="Show Sbt Console"
+ menubarPath="project/additions"
+ style="toggle"
+ tooltip="Show the Sbt console for project">
</action>
</actionSet>
</extension>
@@ -164,7 +164,7 @@ class ScalaPlugin extends AbstractUIPlugin with IResourceChangeListener with IEl
case Some(scalaProject) =>
projects.remove(project)
println("resetting compilers for " + project.getName)
- scalaProject.resetCompilers
+ scalaProject.preClose()
case None =>
}
}
@@ -5,6 +5,7 @@
package scala.tools.eclipse
+import scala.tools.eclipse.sbt.SbtBuilder
import scala.collection.immutable.Set
import scala.collection.mutable.{ LinkedHashSet, HashMap, HashSet }
@@ -34,6 +35,8 @@ class ScalaProject(val underlying: IProject) {
private var hasBeenBuilt = false
private val resetPendingLock = new Object
private var resetPending = false
+
+ val sbtConsole = new SbtBuilder(this)
case class InvalidCompilerSettings() extends RuntimeException(
"Scala compiler cannot initialize for project: " + underlying.getName +
@@ -517,6 +520,11 @@ class ScalaProject(val underlying: IProject) {
hasBeenBuilt = false
}
+ def preClose() {
+ resetCompilers()
+ sbtConsole.dispose()
+ }
+
def resetCompilers(implicit monitor: IProgressMonitor = null) = {
println("resetting compilers! project: " + this.toString)
resetBuildCompiler
@@ -0,0 +1,211 @@
+package scala.tools.eclipse
+package sbt
+
+
+import java.io._
+import java.util.regex.Pattern
+import org.eclipse.core.resources._
+import org.eclipse.core.runtime.Path
+import org.eclipse.jface.dialogs.MessageDialog
+import org.eclipse.swt.widgets.Display
+import org.eclipse.ui.console._
+import org.eclipse.ui.ide.IDE
+import org.eclipse.ui.PlatformUI
+import scala.concurrent.ops
+import scala.sys.process._
+import scala.tools.eclipse.util.FileUtils
+import scala.tools.eclipse.{ScalaPlugin, ScalaProject}
+import scala.util.matching.Regex._
+import scala.util.matching.Regex
+
+/** Hold together the SBT process, the associated project and the
+ * corresponding Console instance
+ */
+class SbtBuilder(project: ScalaProject) {
+ def console: SbtConsole = getConsole()
+
+ @volatile private var shuttingDown = false
+
+ private var sbtProcess: Process = _
+
+ private val consoleName = "Sbt - %s".format(project.underlying.getName)
+
+ private val consoleManager = ConsolePlugin.getDefault().getConsoleManager()
+
+ /** Return the corresponding Sbt console for this builder.
+ * If the current Console manager has a console registered with this name,
+ * return it. Otherwise create and add a new console to the Console manager.
+ */
+ def getConsole(): SbtConsole = {
+ consoleManager.getConsoles().find(_.getName == consoleName) match {
+ case Some(c: SbtConsole) => c
+ case None => createConsole()
+ }
+ }
+
+ /** Create a new SbtConsole. Install the pattern matcher that adds hyperlinks
+ * for error messages.
+ */
+ private def createConsole(): SbtConsole = {
+ val console = new SbtConsole(consoleName, null)
+ console.setConsoleWidth(140)
+
+ val sourceLocationPattern = """^\[error\]\w*(.*):(\d+):(.*)$"""
+
+ console.addPatternMatchListener(PatMatchListener(sourceLocationPattern, (text, offset) =>
+ sourceLocationPattern.r.findFirstMatchIn(text) match {
+ case Some(m @ Groups(path, lineNr, msg)) =>
+ println("error found at %s:%d:%s".format(path, lineNr.toInt, msg))
+ for (file <- ResourcesPlugin.getWorkspace.getRoot().findFilesForLocation(new Path(path.trim))) {
+ println("added hyperlink for %s".format(file))
+ console.addHyperlink(ErrorHyperlink(file, lineNr.toInt, msg), offset + m.start(1), path.length)
+ }
+
+ case _ => println("something went wrong")
+ }))
+
+
+ consoleManager.addConsoles(Array[IConsole](console))
+ console
+ }
+
+ var consoleOutputStream: OutputStream = _
+
+
+ /** Escape sequences of the form <ESC>[12m. */
+ val escapePattern = """\e\[(\d\d?)(;\d\d?)*m""".r
+
+ private def stripControlSequences(text: String): String =
+ escapePattern.replaceAllIn(text, m => "")
+
+ /** Launch the sbt process and route input and output through
+ * the Console object. Escape sequences are stripped form the output
+ * of Sbt.
+ */
+ def launchSbt() {
+ def filterStream(is: InputStream, f: String => String) = try {
+ val BUFSIZE = 80
+ val buff = new Array[Char](BUFSIZE)
+ val reader = new BufferedReader(new InputStreamReader(is))
+ val writer = new PrintWriter(new OutputStreamWriter(consoleOutputStream), true)
+ var size = reader.read(buff, 0, BUFSIZE)
+ while (size > 0) {
+ val str = new String(buff, 0, size)
+ writer.print(f(str))
+ writer.flush()
+ size = reader.read(buff, 0, BUFSIZE)
+ }
+ } catch {
+ case e: IOException => () // do nothing
+ }
+
+ val pio = new ProcessIO(in => BasicIO.transferFully(console.getInputStream, in),
+ os => filterStream(os, stripControlSequences),
+ es => BasicIO.transferFully(es, consoleOutputStream))
+
+ try {
+ import util.ScalaPluginSettings._
+ loadSbtSettings()
+
+ val wkDir = project.underlying.getLocation()
+ consoleOutputStream = console.newOutputStream()
+
+ val javaCmd = "java" :: sbtJavaArgs.value.split(' ').map(_.trim).toList ::: List("-jar", pathToSbt.value)
+ println("starting sbt in %s (%s)".format(wkDir.toString, javaCmd))
+ shuttingDown = false
+ val builder = Process(javaCmd.toArray, Some(project.underlying.getLocation().toFile))
+ sbtProcess = builder.run(pio)
+
+ ops.spawn {
+ val exitCode = sbtProcess.exitValue()
+ // wait until the process terminates, and close this console
+ println("Sbt finished with exit code: %d".format(exitCode))
+ if (exitCode != 0 && !shuttingDown) Display.getDefault asyncExec new Runnable {
+ def run() {
+ MessageDialog.openInformation(ScalaPlugin.getShell, "Sbt launch error", """Could not start Sbt.
+
+Please check the path to sbt-launch.jar (currently %s)""".format(pathToSbt.value))
+ }
+ }
+ dispose()
+ }
+ } catch {
+ case e =>
+ ScalaPlugin.plugin.logError("Error launching sbt", e)
+ }
+ }
+
+ private def loadSbtSettings() {
+ import util.ScalaPluginSettings._
+
+ val store = ScalaPlugin.plugin.getPreferenceStore
+ pathToSbt.value = store.getString(SettingConverterUtil.convertNameToProperty(pathToSbt.name))
+ sbtJavaArgs.value = store.getString(SettingConverterUtil.convertNameToProperty(sbtJavaArgs.name))
+ }
+
+ def visible: Boolean =
+ sbtProcess ne null
+
+ def showConsole() {
+ if (sbtProcess == null) launchSbt()
+ consoleManager.showConsoleView(console)
+ }
+
+ def dispose() {
+ if (sbtProcess ne null) {
+ shuttingDown = true
+ sbtProcess.destroy
+ sbtProcess = null
+ console.dispose();
+ consoleManager.removeConsoles(Array(console))
+ }
+ }
+}
+
+/** A Pattern match listener that calls `fun' when the given regular expression matches. */
+case class PatMatchListener(regex: String, fun: (String, Int) => Unit) extends IPatternMatchListener with IPatternMatchListenerDelegate {
+ var console: Option[TextConsole] = None
+
+ def getLineQualifier() = null
+
+ def getCompilerFlags() = Pattern.MULTILINE
+
+ def getPattern() = regex
+
+ def connect(console: TextConsole) {
+ this.console = Some(console)
+ }
+
+ def disconnect() {
+ console = None
+ }
+
+ def matchFound(event: PatternMatchEvent) {
+ console match {
+ case Some(c) => fun(c.getDocument().get(event.getOffset(), event.getLength()), event.getOffset)
+ case None => println("invalid match, no text console found.")
+ }
+ }
+}
+
+/** A hyperlink for errors in sbt output. When clicked, it opens the corresponding
+ * editor and selects the affected line.
+ *
+ * Error markers are not persisted (the hyperlink deletes it after the editor is open
+ */
+case class ErrorHyperlink(file: IFile, lineNr: Int, msg: String) extends IHyperlink {
+ def linkActivated() {
+ val marker = FileUtils.createMarker(file, IMarker.SEVERITY_ERROR, msg, lineNr)
+ try {
+ val page = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
+ IDE.openEditor(page, marker, true)
+ } catch {
+ case e: Exception =>
+ println("exception while opening editor")
+ } finally marker.delete()
+ }
+
+ def linkExited() {}
+ def linkEntered() {}
+}
+
@@ -0,0 +1,85 @@
+package scala.tools.eclipse.sbt
+
+import scala.util.matching.Regex
+import org.eclipse.jface.resource.ImageDescriptor
+import org.eclipse.swt.custom.StyleRange
+import org.eclipse.swt.graphics.Color
+import org.eclipse.swt.widgets.Display
+import org.eclipse.swt.SWT
+import org.eclipse.ui.console._
+import org.eclipse.ui.internal.console._
+import scala.collection.mutable.ListBuffer
+
+class SbtConsole(name: String, imgDescriptor: ImageDescriptor = null) extends IOConsole(name, imgDescriptor) {
+
+ val partitioner = new SbtPartitioner(this)
+ partitioner.connect(getDocument());
+
+ override def getPartitioner() = partitioner
+
+ override def dispose() = super.dispose()
+}
+
+
+class SbtPartitioner(console: SbtConsole) extends IOConsolePartitioner(console.getInputStream, console) {
+
+ lazy final val fgColor = Display.getDefault().getSystemColor(SWT.COLOR_BLACK)
+ lazy final val bgColor = Display.getDefault().getSystemColor(SWT.COLOR_WHITE)
+ lazy final val infoColor = Display.getDefault().getSystemColor(SWT.COLOR_DARK_MAGENTA)
+ lazy final val errorColor = Display.getDefault().getSystemColor(SWT.COLOR_RED)
+
+ lazy final val defaultStyle = (fgColor, bgColor)
+
+
+ override def getStyleRanges(off: Int, length: Int): Array[StyleRange] = {
+ val part: IOConsolePartition = getPartition(off).asInstanceOf[IOConsolePartition];
+ val offset = part.getOffset()
+ val text = getDocument().get(offset, part.getLength)
+
+ def stylePattern(regex: Regex, color: Color): List[StyleRange] =
+ for (m <- regex.findAllIn(text).matchData.toList) yield
+ new StyleRange(offset + m.start, m.matched.length, color, bgColor)
+
+ if (text ne null ) {
+ val superStyles = super.getStyleRanges(off, length)
+ val styles = stylePattern("""\[error\]""".r, errorColor) ++ stylePattern("""\[info\]""".r, infoColor)
+
+ superStyles ++ styles.sortBy(_.start)
+ } else new Array[StyleRange](0)
+ }
+
+ import SWT._
+
+ /**
+ * Map ANSI control numbers to SWT color codes. It maps the
+ * foreground numer, add 10 for background
+ *
+ * @see http://www.termsys.demon.co.uk/vtansi.htm
+ */
+ lazy val ansiColors = Map(
+ 30 -> COLOR_BLACK,
+ 31 -> COLOR_RED,
+ 32 -> COLOR_GREEN,
+ 33 -> COLOR_YELLOW,
+ 34 -> COLOR_BLUE,
+ 35 -> COLOR_MAGENTA,
+ 36 -> COLOR_CYAN,
+ 37 -> COLOR_WHITE).withDefaultValue(fgColor)
+
+ def getStyles(text: String): List[(Color, Color)] = {
+ var fg = fgColor
+ var bg = bgColor
+ var start = 0
+ var length = 0
+
+// var pattern = """[(\d\d?)(;\d\d?)*m""".r
+//
+// for (m <- pattern.findAllIn(text)) match {
+// case Groups(attr1, rest:_*) =>
+// length =
+// attr1.toInt
+// }
+//
+ Nil
+ }
+}
Oops, something went wrong.

0 comments on commit d880092

Please sign in to comment.