Skip to content
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...
1 parent 400c8a1 commit d880092e0cc48c8be1933fb968931baf70383d9b @dragos dragos committed Apr 19, 2011
View
9 org.scala-ide.sdt.core/plugin.xml
@@ -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>
View
2 org.scala-ide.sdt.core/src/scala/tools/eclipse/ScalaPlugin.scala
@@ -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 =>
}
}
View
8 org.scala-ide.sdt.core/src/scala/tools/eclipse/ScalaProject.scala
@@ -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
View
211 org.scala-ide.sdt.core/src/scala/tools/eclipse/sbt/SbtBuilder.scala
@@ -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() {}
+}
+
View
85 org.scala-ide.sdt.core/src/scala/tools/eclipse/sbt/SbtConsole.scala
@@ -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
+ }
+}
View
73 org.scala-ide.sdt.core/src/scala/tools/eclipse/sbt/ShowSbtConsoleAction.scala
@@ -0,0 +1,73 @@
+package scala.tools.eclipse
+
+package sbt
+
+import scala.tools.eclipse.ScalaPlugin
+import org.eclipse.core.resources._
+import org.eclipse.core.runtime.IAdaptable
+import org.eclipse.jdt.core.IJavaElement
+import org.eclipse.jface.action.IAction
+import org.eclipse.jface.viewers.{IStructuredSelection, ISelection}
+import org.eclipse.ui.console._
+import org.eclipse.ui._
+import scala.tools.eclipse.util.EclipseUtils._
+
+/** Show the Sbt console for the current project.
+ */
+class ShowSbtConsoleAction extends IWorkbenchWindowActionDelegate {
+
+ def performAction(project: IProject, action: IAction) {
+ val scalaProject = ScalaPlugin.plugin.getScalaProject(project)
+ if (scalaProject.sbtConsole.visible) {
+ scalaProject.sbtConsole.dispose()
+ action.setChecked(false)
+ } else {
+ scalaProject.sbtConsole.showConsole()
+ action.setChecked(true)
+ }
+ }
+
+ private var parentWindow: IWorkbenchWindow = null
+
+ override def init(window: IWorkbenchWindow) {
+ parentWindow = window
+ }
+
+ def dispose = {}
+
+ def run(action: IAction) {
+ if (currentProject.isDefined)
+ performAction(currentProject.get, action)
+ else
+ println("Couldn't figure out current project.")
+ }
+
+ def editedProject: Option[IProject] = for {
+ w <- Option(parentWindow)
+ page <- Option(w.getActivePage)
+ editor <- Option(page.getActiveEditor)
+ input <- Option(editor.getEditorInput)
+ res <- input.adaptToSafe[IResource]
+ proj <- Option(res.getProject())
+ } yield proj;
+
+ var currentProject: Option[IProject] = None
+
+ /** Enable the menu item only for open projects. */
+ def selectionChanged(action: IAction, selection: ISelection) {
+ def setProject(p: Option[IProject]) = {
+ currentProject = p orElse editedProject
+ action.setEnabled(currentProject.isDefined && currentProject.get.isOpen)
+ }
+
+ selection match {
+ case structuredSel: IStructuredSelection =>
+ structuredSel.getFirstElement() match {
+ case el: IProject => setProject(Some(el))
+ case adaptable: IAdaptable => setProject(adaptable.adaptToSafe[IProject])
+ case _ => setProject(None)
+ }
+ case _ => setProject(None)
+ }
+ }
+}
View
40 org.scala-ide.sdt.core/src/scala/tools/eclipse/util/FileUtils.scala
@@ -60,31 +60,35 @@ object FileUtils {
def buildError(file : IFile, severity : Int, msg : String, offset : Int, length : Int, line : Int, monitor : IProgressMonitor) =
file.getWorkspace.run(new IWorkspaceRunnable {
def run(monitor : IProgressMonitor) = {
- val mrk = file.createMarker(plugin.problemMarkerId)
- mrk.setAttribute(IMarker.SEVERITY, severity)
-
- // Marker attribute values are limited to <= 65535 bytes and setAttribute will assert if they
- // exceed this. To guard against this we trim to <= 21000 characters ... see
- // org.eclipse.core.internal.resources.MarkerInfo.checkValidAttribute for justification
- // of this arbitrary looking number
- val maxMarkerLen = 21000
- val trimmedMsg = msg.take(maxMarkerLen)
-
- val attrValue = trimmedMsg.map {
- case '\n' | '\r' => ' '
- case c => c
- }
-
- mrk.setAttribute(IMarker.MESSAGE , attrValue)
-
+ val mrk = createMarker(file, severity, msg, line)
if (offset != -1) {
mrk.setAttribute(IMarker.CHAR_START, offset)
mrk.setAttribute(IMarker.CHAR_END, offset + length + 1)
- mrk.setAttribute(IMarker.LINE_NUMBER, line)
}
}
}, monitor)
+ def createMarker(file: IFile, severity: Int, msg: String, line: Int): IMarker = {
+ val mrk = file.createMarker(plugin.problemMarkerId)
+ mrk.setAttribute(IMarker.SEVERITY, severity)
+
+ // Marker attribute values are limited to <= 65535 bytes and setAttribute will assert if they
+ // exceed this. To guard against this we trim to <= 21000 characters ... see
+ // org.eclipse.core.internal.resources.MarkerInfo.checkValidAttribute for justification
+ // of this arbitrary looking number
+ val maxMarkerLen = 21000
+ val trimmedMsg = msg.take(maxMarkerLen)
+
+ val attrValue = trimmedMsg.map {
+ case '\n' | '\r' => ' '
+ case c => c
+ }
+
+ mrk.setAttribute(IMarker.MESSAGE, attrValue)
+ mrk.setAttribute(IMarker.LINE_NUMBER, line)
+ mrk
+ }
+
def task(file : IFile, tag : String, msg : String, priority : String, offset : Int, length : Int, line : Int, monitor : IProgressMonitor) =
file.getWorkspace.run(new IWorkspaceRunnable {
def run(monitor : IProgressMonitor) = {
View
5 org.scala-ide.sdt.core/src/scala/tools/eclipse/util/IDESettings.scala
@@ -31,7 +31,8 @@ object IDESettings {
}
def pluginSettings: List[Box] = {
- List(Box("Scala Plugin Debugging", List(YPlugininfo)))
+ List(Box("Scala Plugin Debugging", List(YPlugininfo)),
+ Box("Sbt", List(pathToSbt, sbtJavaArgs)))
}
def buildManagerSettings: List[Box] = {
@@ -42,4 +43,6 @@ object IDESettings {
object ScalaPluginSettings extends Settings {
val YPlugininfo = BooleanSetting("-plugininfo", "Enable logging of the Scala Plugin info")
val buildManager = ChoiceSetting("-buildmanager", "which", "Build manager to use", List("refined"), "refined")
+ val pathToSbt = StringSetting("-pathToSbt", "path", "The full path to sbt-launch.jar", "/usr/bin/sbt-launch.jar")
+ val sbtJavaArgs = StringSetting("-sbtJavaArgs", "", "Additional JVM arguments", "-Xmx1000M")
}

0 comments on commit d880092

Please sign in to comment.
Something went wrong with that request. Please try again.