diff --git a/build.sbt b/build.sbt index 7faaeef45..569d63969 100644 --- a/build.sbt +++ b/build.sbt @@ -289,6 +289,7 @@ val completeProj = (project in file("internal") / "util-complete") testedBaseSettings, name := "Completion", libraryDependencies += jline, + libraryDependencies += jline3, mimaSettings, // Parser is used publicly, so we can't break bincompat. mimaBinaryIssueFilters := Seq( @@ -343,12 +344,20 @@ lazy val utilPosition = (project in file("internal") / "util-position") lazy val utilLogging = (project in file("internal") / "util-logging") .enablePlugins(ContrabandPlugin, JsonCodecPlugin) - .dependsOn(utilInterface) + .dependsOn(utilInterface, collectionProj) .settings( utilCommonSettings, name := "Util Logging", libraryDependencies ++= - Seq(jline, log4jApi, log4jCore, disruptor, sjsonNewScalaJson.value, scalaReflect.value), + Seq( + jline, + jline3, + log4jApi, + log4jCore, + disruptor, + sjsonNewScalaJson.value, + scalaReflect.value + ), libraryDependencies ++= Seq(scalacheck % "test", scalatest % "test"), libraryDependencies ++= (scalaVersion.value match { case v if v.startsWith("2.12.") => List(compilerPlugin(silencerPlugin)) @@ -1047,8 +1056,7 @@ lazy val sbtClientProj = (project in file("client")) crossPaths := false, exportJars := true, libraryDependencies += jansi, - libraryDependencies += "net.java.dev.jna" % "jna" % "5.5.0", - libraryDependencies += "net.java.dev.jna" % "jna-platform" % "5.5.0", + libraryDependencies += jline3Jansi, libraryDependencies += scalatest % "test", /* * On windows, the raw classpath is too large to be a command argument to an diff --git a/client/src/main/resources/META-INF/native-image/resource-config.json b/client/src/main/resources/META-INF/native-image/resource-config.json index 26dd35cca..0bc65dde4 100644 --- a/client/src/main/resources/META-INF/native-image/resource-config.json +++ b/client/src/main/resources/META-INF/native-image/resource-config.json @@ -1,7 +1,20 @@ { "resources":[ {"pattern":"jline/console/completer/CandidateListCompletionHandler.properties"}, - {"pattern":"library.properties"}, + {"pattern":"org/jline/utils/ansi.caps"}, + {"pattern":"org/jline/utils/capabilities.txt"}, + {"pattern":"org/jline/utils/colors.txt"}, + {"pattern":"org/jline/utils/dumb-color.caps"}, + {"pattern":"org/jline/utils/xterm.caps"}, + {"pattern":"org/jline/utils/xterm-256color.caps"}, + {"pattern":"org/jline/utils/windows-256color.caps"}, + {"pattern":"org/jline/utils/screen-256color.caps"}, + {"pattern":"org/jline/utils/windows.caps"}, + {"pattern":"org/jline/utils/windows-conemu.caps"}, + {"pattern":"org/jline/utils/dumb.caps"}, + {"pattern":"org/jline/utils/windows-vtp.caps"}, + {"pattern":"org/jline/utils/screen.caps"}, + {"pattern":"library.properties"}, {"pattern":"darwin/x86_64/libsbtipcsocket.dylib"}, {"pattern":"linux/x86_64/libsbtipcsocket.so"}, {"pattern":"win32/x86_64/sbtipcsocket.dll"} diff --git a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala index bac94c1de..836a0ba6d 100644 --- a/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala +++ b/internal/util-complete/src/main/scala/sbt/internal/util/LineReader.scala @@ -8,16 +8,28 @@ package sbt.internal.util import java.io._ +import java.util.{ List => JList } import jline.console.ConsoleReader import jline.console.history.{ FileHistory, MemoryHistory } +import org.jline.reader.{ + Candidate, + Completer, + EndOfFileException, + LineReader => JLineReader, + LineReaderBuilder, + ParsedLine, + UserInterruptException, +} import sbt.internal.util.complete.Parser import scala.annotation.tailrec import scala.concurrent.duration._ +import java.nio.channels.ClosedByInterruptException -trait LineReader { +trait LineReader extends AutoCloseable { def readLine(prompt: String, mask: Option[Char] = None): Option[String] + override def close(): Unit = {} } object LineReader { @@ -25,7 +37,67 @@ object LineReader { !java.lang.Boolean.getBoolean("sbt.disable.cont") && Signals.supported(Signals.CONT) val MaxHistorySize = 500 + private def completer(parser: Parser[_]): Completer = new Completer { + def complete(lr: JLineReader, pl: ParsedLine, candidates: JList[Candidate]): Unit = { + Parser.completions(parser, pl.line(), 10).get.foreach { c => + /* + * For commands like `~` that delegate parsing to another parser, the `~` may be + * excluded from the completion result. For example, + * ~testOnly + * might return results like + * 'testOnly ;' + * 'testOnly com.foo.FooSpec' + * ... + * If we use the raw display, JLine will reject the completions because they are + * missing the leading `~`. To workaround this, we append to the result to the + * line provided the line does not end with " ". This fixes the missing `~` in + * the prefix problem. We also need to split the line on space and take the + * last token and append to that otherwise the completion will double print + * the prefix, so that `testOnly com` might expand to something like: + * `testOnly testOnly\ com.foo.FooSpec` instead of `testOnly com.foo.FooSpec`. + */ + if (c.append.nonEmpty) { + if (!pl.line().endsWith(" ")) { + candidates.add(new Candidate(pl.line().split(" ").last + c.append)) + } else { + candidates.add(new Candidate(c.append)) + } + } + } + } + } def createReader( + historyPath: Option[File], + parser: Parser[_], + terminal: Terminal, + prompt: Prompt = Prompt.Running, + ): LineReader = { + val term = JLine3(terminal) + // We may want to consider insourcing LineReader.java from jline. We don't otherwise + // directly need jline3 for sbt. + val reader = LineReaderBuilder.builder().terminal(term).completer(completer(parser)).build() + historyPath.foreach(f => reader.setVariable(JLineReader.HISTORY_FILE, f)) + new LineReader { + override def readLine(prompt: String, mask: Option[Char]): Option[String] = { + try terminal.withRawSystemIn { + Option(mask.map(reader.readLine(prompt, _)).getOrElse(reader.readLine(prompt))) + } catch { + case e: EndOfFileException => + if (terminal == Terminal.console && System.console == null) None + else Some("exit") + case _: IOError => Some("exit") + case _: UserInterruptException | _: ClosedByInterruptException | + _: UncheckedIOException => + throw new InterruptedException + } finally { + terminal.prompt.reset() + term.close() + } + } + } + } + + def createJLine2Reader( historyPath: Option[File], terminal: Terminal, prompt: Prompt = Prompt.Running, @@ -42,7 +114,6 @@ object LineReader { cr.setHistoryEnabled(true) cr } - def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal) def simple( historyPath: Option[File], @@ -230,7 +301,7 @@ final class FullReader( Terminal.console ) protected[this] val reader: ConsoleReader = { - val cr = LineReader.createReader(historyPath, terminal) + val cr = LineReader.createJLine2Reader(historyPath, terminal) sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete) cr } @@ -244,7 +315,7 @@ class SimpleReader private[sbt] ( def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) = this(historyPath, handleCONT, Terminal.console) protected[this] val reader: ConsoleReader = - LineReader.createReader(historyPath, terminal) + LineReader.createJLine2Reader(historyPath, terminal) } object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) { diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala index c89a51204..c6eac168a 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ConsoleAppender.scala @@ -9,6 +9,7 @@ package sbt.internal.util import java.io.{ PrintStream, PrintWriter } import java.lang.StringBuilder +import java.nio.channels.ClosedChannelException import java.util.concurrent.atomic.{ AtomicBoolean, AtomicInteger } import org.apache.logging.log4j.core.appender.AbstractAppender @@ -394,7 +395,8 @@ class ConsoleAppender private[ConsoleAppender] ( override def append(event: XLogEvent): Unit = { val level = ConsoleAppender.toLevel(event.getLevel) val message = event.getMessage - appendMessage(level, message) + try appendMessage(level, message) + catch { case _: ClosedChannelException => } } /** diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala b/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala index 44d00af8c..19af0024a 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/EscHelpers.scala @@ -181,9 +181,10 @@ object EscHelpers { else res(index) = 32 case 'm' => case ';' => state = csi - case _ => + case b => state = csi } digit.clear() + case b if state == esc => state = 0 case b => res(index) = b index += 1 diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala new file mode 100644 index 000000000..eadd09ae6 --- /dev/null +++ b/internal/util-logging/src/main/scala/sbt/internal/util/JLine3.scala @@ -0,0 +1,223 @@ +/* + * sbt + * Copyright 2011 - 2018, Lightbend, Inc. + * Copyright 2008 - 2010, Mark Harrah + * Licensed under Apache License 2.0 (see LICENSE) + */ + +package sbt.internal.util + +import java.io.{ EOFException, InputStream, OutputStream, PrintWriter } +import java.nio.charset.Charset +import java.util.{ Arrays, EnumSet } +import java.util.concurrent.atomic.AtomicBoolean +import org.jline.utils.InfoCmp.Capability +import org.jline.utils.{ NonBlocking, OSUtils } +import org.jline.terminal.{ Attributes, Size, Terminal => JTerminal } +import org.jline.terminal.Terminal.SignalHandler +import org.jline.terminal.impl.AbstractTerminal +import org.jline.terminal.impl.jansi.JansiSupportImpl +import org.jline.terminal.impl.jansi.win.JansiWinSysTerminal +import scala.collection.JavaConverters._ +import scala.util.Try + +private[util] object JLine3 { + private val capabilityMap = Capability + .values() + .map { c => + c.toString -> c + } + .toMap + + private[util] def system = { + /* + * For reasons that are unclear to me, TerminalBuilder fails to build + * windows terminals. The instructions about the classpath did not work: + * https://stackoverflow.com/questions/52851232/jline3-issues-with-windows-terminal + * We can deconstruct what TerminalBuilder does and inline it for now. + * It is possible that this workaround will break WSL but I haven't checked that. + */ + if (Util.isNonCygwinWindows) { + val support = new JansiSupportImpl + val winConsole = support.isWindowsConsole(); + try { + val term = JansiWinSysTerminal.createTerminal( + "console", + "ansi", + OSUtils.IS_CONEMU, + Charset.forName("UTF-8"), + -1, + false, + SignalHandler.SIG_DFL, + true + ) + term.disableScrolling() + term + } catch { + case _: Exception => + org.jline.terminal.TerminalBuilder + .builder() + .system(false) + .paused(true) + .jansi(true) + .streams(Terminal.console.inputStream, Terminal.console.outputStream) + .build() + } + } else { + org.jline.terminal.TerminalBuilder + .builder() + .system(System.console != null) + .paused(true) + .jna(false) + .jansi(true) + .build() + } + } + private[sbt] def apply(term: Terminal): JTerminal = { + new AbstractTerminal(term.name, "ansi", Charset.forName("UTF-8"), SignalHandler.SIG_DFL) { + val closed = new AtomicBoolean(false) + setOnClose { () => + if (closed.compareAndSet(false, true)) { + // This is necessary to shutdown the non blocking input reader + // so that it doesn't keep blocking + term.inputStream match { + case w: Terminal.WriteableInputStream => w.cancel() + case _ => + } + } + } + parseInfoCmp() + override val input: InputStream = new InputStream { + override def read: Int = { + val res = try term.inputStream.read + catch { case _: InterruptedException => -2 } + if (res == 4 && term.prompt.render().endsWith(term.prompt.mkPrompt())) + throw new EOFException + res + } + } + override val output: OutputStream = new OutputStream { + override def write(b: Int): Unit = write(Array[Byte](b.toByte)) + override def write(b: Array[Byte]): Unit = if (!closed.get) term.withPrintStream { ps => + term.prompt match { + case a: Prompt.AskUser => a.write(b) + case _ => + } + ps.write(b) + } + override def write(b: Array[Byte], offset: Int, len: Int) = + write(Arrays.copyOfRange(b, offset, offset + len)) + override def flush(): Unit = term.withPrintStream(_.flush()) + } + + override val reader = + NonBlocking.nonBlocking(term.name, input, Charset.defaultCharset()) + override val writer: PrintWriter = new PrintWriter(output, true) + /* + * For now assume that the terminal capabilities for client and server + * are the same. + */ + override def getStringCapability(cap: Capability): String = { + term.getStringCapability(cap.toString, jline3 = true) + } + override def getNumericCapability(cap: Capability): Integer = { + term.getNumericCapability(cap.toString, jline3 = true) + } + override def getBooleanCapability(cap: Capability): Boolean = { + term.getBooleanCapability(cap.toString, jline3 = true) + } + def getAttributes(): Attributes = attributesFromMap(term.getAttributes) + def getSize(): Size = new Size(term.getWidth, term.getHeight) + def setAttributes(a: Attributes): Unit = term.setAttributes(toMap(a)) + def setSize(size: Size): Unit = term.setSize(size.getColumns, size.getRows) + + /** + * Override enterRawMode because the default implementation modifies System.in + * to be non-blocking which means it immediately returns -1 if there is no + * data available, which is not desirable for us. + */ + override def enterRawMode(): Attributes = enterRawModeImpl(this) + } + } + private def enterRawModeImpl(term: JTerminal): Attributes = { + val prvAttr = term.getAttributes() + val newAttr = new Attributes(prvAttr) + newAttr.setLocalFlags( + EnumSet + .of(Attributes.LocalFlag.ICANON, Attributes.LocalFlag.ECHO, Attributes.LocalFlag.IEXTEN), + false + ) + newAttr.setInputFlags( + EnumSet + .of(Attributes.InputFlag.IXON, Attributes.InputFlag.ICRNL, Attributes.InputFlag.INLCR), + false + ) + term.setAttributes(newAttr) + prvAttr + } + private[util] def enterRawMode(term: JTerminal): Map[String, String] = + toMap(enterRawModeImpl(term)) + private[util] def toMap(jattributes: Attributes): Map[String, String] = { + val result = new java.util.LinkedHashMap[String, String] + result.put( + "iflag", + jattributes.getInputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "oflag", + jattributes.getOutputFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "cflag", + jattributes.getControlFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "lflag", + jattributes.getLocalFlags.iterator.asScala.map(_.name.toLowerCase).mkString(" ") + ) + result.put( + "cchars", + jattributes.getControlChars.entrySet.iterator.asScala + .map { e => + s"${e.getKey.name.toLowerCase},${e.getValue}" + } + .mkString(" ") + ) + result.asScala.toMap + } + private[this] val iflagMap: Map[String, Attributes.InputFlag] = + Attributes.InputFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val oflagMap: Map[String, Attributes.OutputFlag] = + Attributes.OutputFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val cflagMap: Map[String, Attributes.ControlFlag] = + Attributes.ControlFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val lflagMap: Map[String, Attributes.LocalFlag] = + Attributes.LocalFlag.values.map(f => f.name.toLowerCase -> f).toMap + private[this] val charMap: Map[String, Attributes.ControlChar] = + Attributes.ControlChar.values().map(f => f.name.toLowerCase -> f).toMap + private[util] def attributesFromMap(map: Map[String, String]): Attributes = { + val attributes = new Attributes + map.get("iflag").foreach { flags => + flags.split(" ").foreach(f => iflagMap.get(f).foreach(attributes.setInputFlag(_, true))) + } + map.get("oflag").foreach { flags => + flags.split(" ").foreach(f => oflagMap.get(f).foreach(attributes.setOutputFlag(_, true))) + } + map.get("cflag").foreach { flags => + flags.split(" ").foreach(f => cflagMap.get(f).foreach(attributes.setControlFlag(_, true))) + } + map.get("lflag").foreach { flags => + flags.split(" ").foreach(f => lflagMap.get(f).foreach(attributes.setLocalFlag(_, true))) + } + map.get("cchars").foreach { chars => + chars.split(" ").foreach { keyValue => + keyValue.split(",") match { + case Array(k, v) => + Try(v.toInt).foreach(i => charMap.get(k).foreach(c => attributes.setControlChar(c, i))) + case _ => + } + } + } + attributes + } +} diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala index 13332812b..d8b77ef53 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/ProgressState.scala @@ -14,7 +14,6 @@ import sbt.internal.util.ConsoleAppender.{ ClearScreenAfterCursor, CursorLeft1000, DeleteLine, - cursorLeft, cursorUp, } @@ -33,6 +32,10 @@ private[sbt] final class ProgressState( blankZone, new AtomicReference(new ArrayBuffer[Byte]), ) + def currentLine: Option[String] = + new String(currentLineBytes.get.toArray, "UTF-8").linesIterator.toSeq.lastOption + .map(EscHelpers.stripColorsAndMoves) + .filter(_.nonEmpty) def reset(): Unit = { progressLines.set(Nil) padding.set(0) @@ -44,8 +47,9 @@ private[sbt] final class ProgressState( currentLineBytes.set(new ArrayBuffer[Byte]) } - private[util] def addBytes(terminal: Terminal, bytes: ArrayBuffer[Byte]): Unit = { - val previous = currentLineBytes.get + private[this] val lineSeparatorBytes: Array[Byte] = System.lineSeparator.getBytes("UTF-8") + private[util] def addBytes(terminal: Terminal, bytes: Seq[Byte]): Unit = { + val previous: ArrayBuffer[Byte] = currentLineBytes.get val padding = this.padding.get val prevLineCount = if (padding > 0) terminal.lineCount(new String(previous.toArray)) else 0 previous ++= bytes @@ -54,6 +58,16 @@ private[sbt] final class ProgressState( val diff = newLineCount - prevLineCount this.padding.set(math.max(padding - diff, 0)) } + val lines = new String(previous.toArray, "UTF-8") + if (lines.contains(System.lineSeparator)) { + currentLineBytes.set(new ArrayBuffer[Byte]) + if (!lines.endsWith(System.lineSeparator)) { + lines + .split(System.lineSeparator) + .lastOption + .foreach(currentLineBytes.get ++= _.getBytes("UTF-8")) + } + } } private[util] def printPrompt(terminal: Terminal, printStream: PrintStream): Unit = @@ -62,31 +76,50 @@ private[sbt] final class ProgressState( val pmpt = prefix.getBytes ++ terminal.prompt.render().getBytes pmpt.foreach(b => printStream.write(b & 0xFF)) } - private[util] def reprint(terminal: Terminal, printStream: PrintStream): Unit = { - printPrompt(terminal, printStream) - if (progressLines.get.nonEmpty) { - val lines = printProgress(terminal, terminal.getLastLine.getOrElse("")) - printStream.print(ClearScreenAfterCursor + lines) - } + private[util] def write( + terminal: Terminal, + bytes: Array[Byte], + printStream: PrintStream, + hasProgress: Boolean + ): Unit = { + addBytes(terminal, bytes) + if (hasProgress && terminal.prompt != Prompt.Loading) { + terminal.prompt match { + case a: Prompt.AskUser if a.render.nonEmpty => + printStream.print(System.lineSeparator + ClearScreenAfterCursor + CursorLeft1000) + printStream.flush() + case _ => + } + printStream.write(bytes) + printStream.write(ClearScreenAfterCursor.getBytes("UTF-8")) + printStream.flush() + if (bytes.endsWith(lineSeparatorBytes)) { + if (progressLines.get.nonEmpty) { + val lastLine = terminal.prompt match { + case a: Prompt.AskUser => a.render() + case _ => currentLine.getOrElse("") + } + val lines = printProgress(terminal, lastLine) + printStream.print(ClearScreenAfterCursor + lines) + } + } + printPrompt(terminal, printStream) + } else printStream.write(bytes) } - private[util] def printProgress( - terminal: Terminal, - lastLine: String - ): String = { + private[util] def printProgress(terminal: Terminal, lastLine: String): String = { val previousLines = progressLines.get if (previousLines.nonEmpty) { val currentLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_)) val (height, width) = terminal.getLineHeightAndWidth(lastLine) - val left = cursorLeft(1000) // resets the position to the left val offset = width > 0 val pad = math.max(padding.get - height, 0) - val start = (if (offset) "\n" else "") + val start = (if (offset) s"\n$CursorLeft1000" else "") val totalSize = currentLength + blankZone + pad - val blank = left + s"\n$DeleteLine" * (totalSize - currentLength) + val blank = CursorLeft1000 + s"\n$DeleteLine" * (totalSize - currentLength) val lines = previousLines.mkString(DeleteLine, s"\n$DeleteLine", s"\n$DeleteLine") val resetCursorUp = cursorUp(totalSize + (if (offset) 1 else 0)) - val resetCursor = resetCursorUp + left + lastLine + val resetCursor = resetCursorUp + CursorLeft1000 + lastLine start + blank + lines + resetCursor } else { ClearScreenAfterCursor @@ -108,6 +141,7 @@ private[sbt] object ProgressState { terminal: Terminal ): Unit = { val state = terminal.progressState + val isAskUser = terminal.prompt.isInstanceOf[Prompt.AskUser] val isRunning = terminal.prompt == Prompt.Running val isBatch = terminal.prompt == Prompt.Batch val isWatch = terminal.prompt == Prompt.Watch @@ -115,31 +149,27 @@ private[sbt] object ProgressState { if (terminal.isSupershellEnabled) { if (!pe.skipIfActive.getOrElse(false) || (!isRunning && !isBatch)) { terminal.withPrintStream { ps => - val info = - if ((isRunning || isBatch || noPrompt) && pe.channelName - .fold(true)(_ == terminal.name)) { - pe.items.map { item => - val elapsed = item.elapsedMicros / 1000000L - s" | => ${item.name} ${elapsed}s" - } - } else { - pe.command.toSeq.flatMap { cmd => - val tail = if (isWatch) Nil else "enter 'cancel' to stop evaluation" :: Nil - s"sbt server is running '$cmd'" :: tail - } + val commandFromThisTerminal = pe.channelName.fold(true)(_ == terminal.name) + val info = if ((isRunning || isBatch || noPrompt) && commandFromThisTerminal) { + pe.items.map { item => + val elapsed = item.elapsedMicros / 1000000L + s" | => ${item.name} ${elapsed}s" } + } else { + pe.command.toSeq.flatMap { cmd => + val tail = if (isWatch) Nil else "enter 'cancel' to stop evaluation" :: Nil + s"sbt server is running '$cmd'" :: tail + } + } val currentLength = info.foldLeft(0)(_ + terminal.lineCount(_)) val previousLines = state.progressLines.getAndSet(info) val prevLength = previousLines.foldLeft(0)(_ + terminal.lineCount(_)) - val lastLine = terminal.prompt match { - case Prompt.Running | Prompt.Batch => terminal.getLastLine.getOrElse("") - case a => a.render() - } val prevSize = prevLength + state.padding.get - val newPadding = math.max(0, prevSize - currentLength) - state.padding.set(newPadding) + val lastLine = + if (isAskUser) terminal.prompt.render() else terminal.getLastLine.getOrElse("") + state.padding.set(math.max(0, prevSize - currentLength)) state.printPrompt(terminal, ps) ps.print(state.printProgress(terminal, lastLine)) ps.flush() diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Prompt.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Prompt.scala index bd50e9e75..eb5e4a660 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/Prompt.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Prompt.scala @@ -7,39 +7,29 @@ package sbt.internal.util -import java.io.OutputStream import java.util.concurrent.LinkedBlockingQueue - import scala.collection.JavaConverters._ private[sbt] sealed trait Prompt { def mkPrompt: () => String def render(): String - def wrappedOutputStream(terminal: Terminal): OutputStream + def reset(): Unit } private[sbt] object Prompt { private[sbt] case class AskUser(override val mkPrompt: () => String) extends Prompt { - private[this] val bytes = new LinkedBlockingQueue[Int] - override def wrappedOutputStream(terminal: Terminal): OutputStream = new OutputStream { - override def write(b: Int): Unit = { - if (b == 10) bytes.clear() - else bytes.put(b) - terminal.withPrintStream { p => - p.write(b) - p.flush() - } - } - override def flush(): Unit = terminal.withPrintStream(_.flush()) + private[this] val bytes = new LinkedBlockingQueue[Byte] + def write(b: Array[Byte]): Unit = b.foreach(bytes.put) + override def render(): String = { + val res = new String(bytes.asScala.toArray, "UTF-8") + if (res.endsWith(System.lineSeparator)) "" else res } - - override def render(): String = - EscHelpers.stripMoves(new String(bytes.asScala.toArray.map(_.toByte))) + override def reset(): Unit = bytes.clear() } private[sbt] trait NoPrompt extends Prompt { override val mkPrompt: () => String = () => "" override def render(): String = "" - override def wrappedOutputStream(terminal: Terminal): OutputStream = terminal.outputStream + override def reset(): Unit = {} } private[sbt] case object Running extends NoPrompt private[sbt] case object Batch extends NoPrompt diff --git a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala index f8c9328d7..d59679ee5 100644 --- a/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala +++ b/internal/util-logging/src/main/scala/sbt/internal/util/Terminal.scala @@ -7,19 +7,17 @@ package sbt.internal.util -import java.io.{ InputStream, OutputStream, PrintStream } +import java.io.{ InputStream, InterruptedIOException, OutputStream, PrintStream } import java.nio.channels.ClosedChannelException -import java.util.Locale +import java.util.{ Arrays, Locale } import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } -import java.util.concurrent.{ ConcurrentHashMap, Executors, LinkedBlockingQueue, TimeUnit } +import java.util.concurrent.{ ArrayBlockingQueue, Executors, LinkedBlockingQueue, TimeUnit } import jline.DefaultTerminal2 import jline.console.ConsoleReader -import sbt.internal.util.ConsoleAppender.{ ClearScreenAfterCursor, CursorLeft1000 } - import scala.annotation.tailrec -import scala.collection.mutable.ArrayBuffer import scala.util.Try +import scala.util.control.NonFatal trait Terminal extends AutoCloseable { @@ -111,9 +109,12 @@ trait Terminal extends AutoCloseable { */ private[sbt] def getLastLine: Option[String] - private[sbt] def getBooleanCapability(capability: String): Boolean - private[sbt] def getNumericCapability(capability: String): Int - private[sbt] def getStringCapability(capability: String): String + private[sbt] def getBooleanCapability(capability: String, jline3: Boolean): Boolean + private[sbt] def getNumericCapability(capability: String, jline3: Boolean): Integer + private[sbt] def getStringCapability(capability: String, jline3: Boolean): String + private[sbt] def getAttributes: Map[String, String] + private[sbt] def setAttributes(attributes: Map[String, String]): Unit + private[sbt] def setSize(width: Int, height: Int): Unit private[sbt] def name: String private[sbt] def withRawSystemIn[T](f: => T): T = f @@ -142,7 +143,8 @@ trait Terminal extends AutoCloseable { val len = l.length if (width > 0 && len > 0) (len - 1 + width) / width else 0 } - lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_)) + if (lines.nonEmpty) lines.tail.foldLeft(lines.headOption.fold(0)(count))(_ + count(_)) + else 0 } } @@ -188,13 +190,13 @@ object Terminal { override def enableInterruptCharacter(): Unit = {} override def getOutputEncoding: String = null override def getBooleanCapability(capability: String): Boolean = { - term.getBooleanCapability(capability) + term.getBooleanCapability(capability, jline3 = false) } override def getNumericCapability(capability: String): Integer = { - term.getNumericCapability(capability) + term.getNumericCapability(capability, jline3 = false) } override def getStringCapability(capability: String): String = { - term.getStringCapability(capability) + term.getStringCapability(capability, jline3 = false) } } } @@ -253,18 +255,45 @@ object Terminal { */ private[sbt] def restore(): Unit = console.toJLine.restore() + private[this] val hasProgress: AtomicBoolean = new AtomicBoolean(false) + /** * + * @param progress toggles whether or not the console terminal has progress * @param f the thunk to run * @tparam T the result type of the thunk * @return the result of the thunk */ - private[sbt] def withStreams[T](f: => T): T = + private[sbt] def withStreams[T](isServer: Boolean)(f: => T): T = if (System.getProperty("sbt.io.virtual", "true") == "true") { + hasProgress.set(isServer) try withOut(withIn(f)) finally { jline.TerminalFactory.reset() - console.close() + if (isServer) { + console match { + case c: ConsoleTerminal if !isWindows => + /* + * Entering raw mode in this way causes the standard in InputStream + * to become non-blocking. After we set it to non-blocking, we spin + * up a thread that reads from the inputstream and the resets it + * back to blocking mode. We can then close the console. We do + * this on a background thread to avoid blocking sbt's exit. + */ + val prev = c.system.enterRawMode() + val runnable: Runnable = () => { + c.inputStream.read() + c.system.setAttributes(prev) + c.close() + } + val thread = new Thread(runnable, "sbt-console-background-close") + thread.setDaemon(true) + thread.start() + case c => c.close() + } + } else { + console.close() + } } } else f @@ -281,10 +310,16 @@ object Terminal { override def isEchoEnabled: Boolean = t.isEchoEnabled override def isSuccessEnabled: Boolean = t.isSuccessEnabled override def isSupershellEnabled: Boolean = t.isSupershellEnabled - override def getBooleanCapability(capability: String): Boolean = - t.getBooleanCapability(capability) - override def getNumericCapability(capability: String): Int = t.getNumericCapability(capability) - override def getStringCapability(capability: String): String = t.getStringCapability(capability) + override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = + t.getBooleanCapability(capability, jline3) + override def getNumericCapability(capability: String, jline3: Boolean): Integer = + t.getNumericCapability(capability, jline3) + override def getStringCapability(capability: String, jline3: Boolean): String = + t.getStringCapability(capability, jline3) + override private[sbt] def getAttributes: Map[String, String] = t.getAttributes + override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = + t.setAttributes(attributes) + override private[sbt] def setSize(width: Int, height: Int): Unit = t.setSize(width, height) override def withRawSystemIn[T](f: => T): T = t.withRawSystemIn(f) override def withCanonicalIn[T](f: => T): T = t.withCanonicalIn(f) override def printStream: PrintStream = t.printStream @@ -322,18 +357,35 @@ object Terminal { } } - private[this] val originalOut = System.out + val sepBytes = System.lineSeparator.getBytes("UTF-8") + private class LinePrintStream(outputStream: OutputStream) + extends PrintStream(outputStream, true) { + override def println(s: String): Unit = synchronized { + out.write(s.getBytes("UTF-8") ++ sepBytes) + out.flush() + } + } + private[this] val originalOut = new LinePrintStream(System.out) private[this] val originalIn = System.in private[sbt] class WriteableInputStream(in: InputStream, name: String) extends InputStream with AutoCloseable { - final def write(bytes: Int*): Unit = bytes.foreach(i => buffer.put(i)) + final def write(bytes: Int*): Unit = waiting.synchronized { + waiting.poll match { + case null => + bytes.foreach(b => buffer.put(b)) + case w => + if (bytes.length > 1) bytes.tail.foreach(b => buffer.put(b)) + bytes.headOption.foreach(b => w.put(b)) + } + } private[this] val executor = Executors.newSingleThreadExecutor(r => new Thread(r, s"sbt-$name-input-reader")) private[this] val buffer = new LinkedBlockingQueue[Integer] private[this] val closed = new AtomicBoolean(false) - private[this] val resultQueue = new LinkedBlockingQueue[LinkedBlockingQueue[Int]] - private[this] val waiting = ConcurrentHashMap.newKeySet[LinkedBlockingQueue[Int]] + private[this] val readQueue = new LinkedBlockingQueue[Unit] + private[this] val waiting = new ArrayBlockingQueue[LinkedBlockingQueue[Integer]](1) + private[this] val readThread = new AtomicReference[Thread] /* * Starts a loop that waits for consumers of the InputStream to call read. * When read is called, we enqueue a `LinkedBlockingQueue[Int]` to which @@ -354,11 +406,14 @@ object Terminal { */ private[this] val runnable: Runnable = () => { @tailrec def impl(): Unit = { - val result = resultQueue.take + val _ = readQueue.take val b = in.read // The downstream consumer may have been interrupted. Buffer the result // when that hapens. - if (waiting.contains(result)) result.put(b) else buffer.put(b) + waiting.poll match { + case null => buffer.put(b) + case q => q.put(b) + } if (b != -1 && !Thread.interrupted()) impl() else closed.set(true) } @@ -370,21 +425,28 @@ object Terminal { if (closed.get) -1 else synchronized { - buffer.poll match { + readThread.set(Thread.currentThread) + try buffer.poll match { case null => - val result = new LinkedBlockingQueue[Int] - waiting.add(result) - resultQueue.offer(result) - try result.take + val result = new LinkedBlockingQueue[Integer] + waiting.synchronized(waiting.put(result)) + readQueue.put(()) + try result.take.toInt catch { case e: InterruptedException => waiting.remove(result) - throw e + -1 } case b if b == -1 => throw new ClosedChannelException - case b => b - } + case b => b.toInt + } finally readThread.set(null) } + def cancel(): Unit = waiting.synchronized { + Option(readThread.getAndSet(null)).foreach(_.interrupt()) + waiting.forEach(_.put(-2)) + waiting.clear() + readQueue.clear() + } override def available(): Int = { buffer.size @@ -524,7 +586,7 @@ object Terminal { } override def flush(): Unit = os.flush() } - private[this] val proxyPrintStream = new PrintStream(proxyOutputStream, true) { + private[this] val proxyPrintStream = new LinePrintStream(proxyOutputStream) { override def toString: String = s"proxyPrintStream($proxyOutputStream)" } private[this] lazy val isWindows = @@ -592,9 +654,21 @@ object Terminal { case t: jline.Terminal2 => t case _ => new DefaultTerminal2(terminal) } - override def init(): Unit = if (alive) terminal.init() - override def restore(): Unit = if (alive) terminal.restore() - override def reset(): Unit = if (alive) terminal.reset() + override def init(): Unit = + if (alive) + try terminal.init() + catch { + case _: InterruptedException => + } + override def restore(): Unit = + if (alive) + try terminal.restore() + catch { + case _: InterruptedException => + } + override def reset(): Unit = + try terminal.reset() + catch { case _: InterruptedException => } override def isSupported: Boolean = terminal.isSupported override def getWidth: Int = props.map(_.width).getOrElse(terminal.getWidth) override def getHeight: Int = props.map(_.height).getOrElse(terminal.getHeight) @@ -650,7 +724,7 @@ object Terminal { fixTerminalProperty() private[sbt] def createReader(term: Terminal, prompt: Prompt): ConsoleReader = { - new ConsoleReader(term.inputStream, prompt.wrappedOutputStream(term), term.toJLine) { + new ConsoleReader(term.inputStream, term.outputStream, term.toJLine) { override def readLine(prompt: String, mask: Character): String = term.withRawSystemIn(super.readLine(prompt, mask)) override def readLine(prompt: String): String = term.withRawSystemIn(super.readLine(prompt)) @@ -662,6 +736,9 @@ object Terminal { case term => term } + private val capabilityMap = + org.jline.utils.InfoCmp.Capability.values().map(c => c.toString -> c).toMap + @deprecated("For compatibility only", "1.4.0") private[sbt] def deprecatedTeminal: jline.Terminal = console.toJLine private class ConsoleTerminal( @@ -669,28 +746,36 @@ object Terminal { in: InputStream, out: OutputStream ) extends TerminalImpl(in, out, "console0") { + private[util] lazy val system = JLine3.system private[this] def isCI = sys.env.contains("BUILD_NUMBER") || sys.env.contains("CI") - override def getWidth: Int = term.getWidth - override def getHeight: Int = term.getHeight + override def getWidth: Int = system.getSize.getColumns + override def getHeight: Int = system.getSize.getRows override def isAnsiSupported: Boolean = term.isAnsiSupported && !isCI - override def isEchoEnabled: Boolean = term.isEchoEnabled + override def isEchoEnabled: Boolean = system.echo() override def isSuccessEnabled: Boolean = true - override def getBooleanCapability(capability: String): Boolean = - term.getBooleanCapability(capability) - override def getNumericCapability(capability: String): Int = - term.getNumericCapability(capability) - override def getStringCapability(capability: String): String = - term.getStringCapability(capability) + override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = + if (jline3) capabilityMap.get(capability).fold(false)(system.getBooleanCapability) + else term.getBooleanCapability(capability) + override def getNumericCapability(capability: String, jline3: Boolean): Integer = + if (jline3) capabilityMap.get(capability).fold(null: Integer)(system.getNumericCapability) + else term.getNumericCapability(capability) + override def getStringCapability(capability: String, jline3: Boolean): String = + if (jline3) capabilityMap.get(capability).fold(null: String)(system.getStringCapability) + else term.getStringCapability(capability) override private[sbt] def restore(): Unit = term.restore() + override private[sbt] def getAttributes: Map[String, String] = + JLine3.toMap(system.getAttributes) + override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = + system.setAttributes(JLine3.attributesFromMap(attributes)) + override private[sbt] def setSize(width: Int, height: Int): Unit = + system.setSize(new org.jline.terminal.Size(width, height)) + override def withRawSystemIn[T](f: => T): T = term.synchronized { - try { - term.init() - term.setEchoEnabled(false) - f - } finally { - term.restore() - term.setEchoEnabled(true) + val prev = JLine3.enterRawMode(system) + try f + catch { case _: InterruptedIOException => throw new InterruptedException } finally { + setAttributes(prev) } } override def isColorEnabled: Boolean = @@ -705,29 +790,33 @@ object Terminal { case "true" => true case _ => false }) + override def close(): Unit = { + try system.close() + catch { case NonFatal(_) => } + super.close() + } } private[sbt] abstract class TerminalImpl private[sbt] ( val in: InputStream, val out: OutputStream, override private[sbt] val name: String ) extends Terminal { - private[this] val directWrite = new AtomicBoolean(false) - private[this] val currentLine = new AtomicReference(new ArrayBuffer[Byte]) - private[this] val lineBuffer = new LinkedBlockingQueue[Byte] - private[this] val flushQueue = new LinkedBlockingQueue[Seq[Byte]] private[this] val writeLock = new AnyRef private[this] val writeableInputStream = in match { case w: WriteableInputStream => w case _ => new WriteableInputStream(in, name) } def throwIfClosed[R](f: => R): R = if (isStopped.get) throw new ClosedChannelException else f + override def getLastLine: Option[String] = progressState.currentLine private val combinedOutputStream = new OutputStream { override def write(b: Int): Unit = { Option(bootOutputStreamHolder.get).foreach(_.write(b)) out.write(b) } - override def write(b: Array[Byte]): Unit = write(b, 0, b.length) + override def write(b: Array[Byte]): Unit = { + write(b, 0, b.length) + } override def write(b: Array[Byte], offset: Int, len: Int): Unit = { Option(bootOutputStreamHolder.get).foreach(_.write(b, offset, len)) out.write(b, offset, len) @@ -740,54 +829,19 @@ object Terminal { override val outputStream = new OutputStream { override def write(b: Int): Unit = throwIfClosed { - writeLock.synchronized { - if (b == Int.MinValue) currentLine.set(new ArrayBuffer[Byte]) - else doWrite(Vector((b & 0xFF).toByte)) - if (b == 10) combinedOutputStream.flush() - } + write(Array((b & 0xFF).toByte)) } - override def write(b: Array[Byte]): Unit = throwIfClosed(write(b, 0, b.length)) - override def write(b: Array[Byte], off: Int, len: Int): Unit = { - throwIfClosed { - writeLock.synchronized { - val lo = math.max(0, off) - val hi = math.min(math.max(off + len, 0), b.length) - doWrite(b.slice(off, off + len).toSeq) - } - } + override def write(b: Array[Byte]): Unit = throwIfClosed { + writeLock.synchronized(doWrite(b)) + } + override def write(b: Array[Byte], offset: Int, length: Int): Unit = throwIfClosed { + write(Arrays.copyOfRange(b, offset, offset + length)) } override def flush(): Unit = combinedOutputStream.flush() - private[this] val clear = s"$CursorLeft1000$ClearScreenAfterCursor" - private def doWrite(bytes: Seq[Byte]): Unit = { - def doWrite(b: Byte): Unit = out.write(b & 0xFF) - val remaining = bytes.foldLeft(new ArrayBuffer[Byte]) { (buf, i) => - if (i == 10) { - progressState.addBytes(TerminalImpl.this, buf) - progressState.clearBytes() - val cl = currentLine.get - if (buf.nonEmpty && isAnsiSupported && cl.isEmpty) clear.getBytes.foreach(doWrite) - combinedOutputStream.write(buf.toArray) - combinedOutputStream.write(10) - currentLine.get match { - case s if s.nonEmpty => currentLine.set(new ArrayBuffer[Byte]) - case _ => - } - if (prompt != Prompt.Loading) progressState.reprint(TerminalImpl.this, rawPrintStream) - new ArrayBuffer[Byte] - } else buf += i - } - if (remaining.nonEmpty) { - val cl = currentLine.get - if (isAnsiSupported && cl.isEmpty) { - clear.getBytes.foreach(doWrite) - } - cl ++= remaining - combinedOutputStream.write(remaining.toArray) - } - combinedOutputStream.flush() - } } - override private[sbt] val printStream: PrintStream = new PrintStream(outputStream, true) + private def doWrite(bytes: Array[Byte]): Unit = + progressState.write(TerminalImpl.this, bytes, rawPrintStream, hasProgress.get) + override private[sbt] val printStream: PrintStream = new LinePrintStream(outputStream) override def inputStream: InputStream = writeableInputStream private[sbt] def write(bytes: Int*): Unit = writeableInputStream.write(bytes: _*) @@ -801,17 +855,7 @@ object Terminal { case _ => (0, 0) } - override def getLastLine: Option[String] = currentLine.get match { - case bytes if bytes.isEmpty => None - case bytes => - // TODO there are ghost characters when the user deletes prompt characters - // when they are given the cancellation option - Some(new String(bytes.toArray).replaceAllLiterally(ClearScreenAfterCursor, "")) - } - - private[this] val rawPrintStream: PrintStream = new PrintStream(combinedOutputStream, true) { - override def close(): Unit = {} - } + private[this] val rawPrintStream: PrintStream = new LinePrintStream(combinedOutputStream) override def withPrintStream[T](f: PrintStream => T): T = writeLock.synchronized(f(rawPrintStream)) @@ -821,12 +865,12 @@ object Terminal { } private[sbt] val NullTerminal = new Terminal { override def close(): Unit = {} - override def getBooleanCapability(capability: String): Boolean = false + override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = false override def getHeight: Int = 0 override def getLastLine: Option[String] = None override def getLineHeightAndWidth(line: String): (Int, Int) = (0, 0) - override def getNumericCapability(capability: String): Int = -1 - override def getStringCapability(capability: String): String = null + override def getNumericCapability(capability: String, jline3: Boolean): Integer = null + override def getStringCapability(capability: String, jline3: Boolean): String = null override def getWidth: Int = 0 override def inputStream: java.io.InputStream = () => { try this.synchronized(this.wait) @@ -839,6 +883,9 @@ object Terminal { override def isSuccessEnabled: Boolean = false override def isSupershellEnabled: Boolean = false override def outputStream: java.io.OutputStream = _ => {} + override private[sbt] def getAttributes: Map[String, String] = Map.empty + override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = {} + override private[sbt] def setSize(width: Int, height: Int): Unit = {} override private[sbt] def name: String = "NullTerminal" override private[sbt] val printStream: java.io.PrintStream = new PrintStream(outputStream, false) diff --git a/internal/util-logging/src/test/scala/sbt/internal/util/CleanStringSpec.scala b/internal/util-logging/src/test/scala/sbt/internal/util/CleanStringSpec.scala index f7ad9185c..9939d668f 100644 --- a/internal/util-logging/src/test/scala/sbt/internal/util/CleanStringSpec.scala +++ b/internal/util-logging/src/test/scala/sbt/internal/util/CleanStringSpec.scala @@ -63,4 +63,11 @@ class CleanStringSpec extends FlatSpec { val colorArrow = new String(Array[Byte](27, 91, 51, 54, 109, 62)) assert(EscHelpers.stripMoves(original) == "foo" + colorArrow + " " + scala.Console.RESET) } + it should "remove unusual escape characters" in { + val original = new String( + Array[Byte](27, 91, 63, 49, 108, 27, 62, 27, 91, 63, 49, 48, 48, 48, 108, 27, 91, 63, 50, 48, + 48, 52, 108) + ) + assert(EscHelpers.stripColorsAndMoves(original).isEmpty) + } } diff --git a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala index 630b50ee3..a9483247e 100644 --- a/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala +++ b/main-command/src/main/scala/sbt/internal/client/NetworkClient.scala @@ -12,7 +12,6 @@ package client import java.io.{ File, IOException, InputStream, PrintStream } import java.lang.ProcessBuilder.Redirect import java.net.Socket -import java.nio.channels.ClosedChannelException import java.nio.file.Files import java.util.UUID import java.util.concurrent.atomic.{ AtomicBoolean, AtomicReference } @@ -43,10 +42,14 @@ import Serialization.{ promptChannel, systemIn, systemOut, + systemOutFlush, terminalCapabilities, terminalCapabilitiesResponse, terminalPropertiesQuery, - terminalPropertiesResponse + terminalPropertiesResponse, + getTerminalAttributes, + setTerminalAttributes, + setTerminalSize, } import NetworkClient.Arguments @@ -199,7 +202,6 @@ class NetworkClient( case _ => (false, None) } if (rebootCommands.nonEmpty) { - if (Terminal.console.getLastLine.isDefined) Terminal.console.printStream.println() rebooting.set(true) attached.set(false) connectionHolder.getAndSet(null) match { @@ -212,7 +214,7 @@ class NetworkClient( rebooting.set(false) rebootCommands match { case Some((execId, cmd)) if execId.nonEmpty => - if (batchMode.get && !pendingResults.contains(execId) && cmd.isEmpty) { + if (batchMode.get && !pendingResults.containsKey(execId) && cmd.nonEmpty) { console.appendLog( Level.Error, s"received request to re-run unknown command '$cmd' after reboot" @@ -230,8 +232,6 @@ class NetworkClient( } else { if (!rebooting.get() && running.compareAndSet(true, false) && log) { if (!arguments.commandArguments.contains(Shutdown)) { - if (Terminal.console.getLastLine.isDefined) - Terminal.console.printStream.println() console.appendLog(Level.Error, "sbt server disconnected") exitClean.set(false) } @@ -306,7 +306,6 @@ class NetworkClient( Some(process) case _ => if (log) { - if (Terminal.console.getLastLine.isDefined) Terminal.console.printStream.println() console.appendLog(Level.Info, "sbt server is booting up") } None @@ -522,17 +521,15 @@ class NetworkClient( } } else Vector() case (`systemOut`, Some(json)) => - Converter.fromJson[Seq[Byte]](json) match { - case Success(params) => - if (params.nonEmpty) { - if (attached.get) { - printStream.write(params.toArray) - printStream.flush() - } - } - case Failure(_) => + Converter.fromJson[Array[Byte]](json) match { + case Success(bytes) if bytes.nonEmpty && attached.get => + synchronized(printStream.write(bytes)) + case _ => } Vector.empty + case (`systemOutFlush`, _) => + synchronized(printStream.flush()) + Vector.empty case (`promptChannel`, _) => batchMode.set(false) Vector.empty @@ -589,16 +586,23 @@ class NetworkClient( } def onRequest(msg: JsonRpcRequestMessage): Unit = { + import sbt.protocol.codec.JsonProtocol._ (msg.method, msg.params) match { case (`terminalCapabilities`, Some(json)) => - import sbt.protocol.codec.JsonProtocol._ Converter.fromJson[TerminalCapabilitiesQuery](json) match { case Success(terminalCapabilitiesQuery) => + val jline3 = terminalCapabilitiesQuery.jline3 val response = TerminalCapabilitiesResponse( - terminalCapabilitiesQuery.boolean.map(Terminal.console.getBooleanCapability), - terminalCapabilitiesQuery.numeric.map(Terminal.console.getNumericCapability), + terminalCapabilitiesQuery.boolean + .map(Terminal.console.getBooleanCapability(_, jline3)), + terminalCapabilitiesQuery.numeric + .map( + c => Option(Terminal.console.getNumericCapability(c, jline3)).fold(-1)(_.toInt) + ), terminalCapabilitiesQuery.string - .map(s => Option(Terminal.console.getStringCapability(s)).getOrElse("null")), + .map( + s => Option(Terminal.console.getStringCapability(s, jline3)).getOrElse("null") + ), ) sendCommandResponse( terminalCapabilitiesResponse, @@ -617,6 +621,37 @@ class NetworkClient( isEchoEnabled = Terminal.console.isEchoEnabled ) sendCommandResponse(terminalPropertiesResponse, response, msg.id) + case (`setTerminalAttributes`, Some(json)) => + Converter.fromJson[TerminalSetAttributesCommand](json) match { + case Success(attributes) => + val attrs = Map( + "iflag" -> attributes.iflag, + "oflag" -> attributes.oflag, + "cflag" -> attributes.cflag, + "lflag" -> attributes.lflag, + "cchars" -> attributes.cchars, + ) + Terminal.console.setAttributes(attrs) + sendCommandResponse("", TerminalSetAttributesResponse(), msg.id) + case Failure(_) => + } + case (`getTerminalAttributes`, _) => + val attrs = Terminal.console.getAttributes + val response = TerminalAttributesResponse( + iflag = attrs.getOrElse("iflag", ""), + oflag = attrs.getOrElse("oflag", ""), + cflag = attrs.getOrElse("cflag", ""), + lflag = attrs.getOrElse("lflag", ""), + cchars = attrs.getOrElse("cchars", ""), + ) + sendCommandResponse("", response, msg.id) + case (`setTerminalSize`, Some(json)) => + Converter.fromJson[TerminalSetSizeCommand](json) match { + case Success(size) => + Terminal.console.setSize(size.width, size.height) + sendCommandResponse("", TerminalSetSizeResponse(), msg.id) + case Failure(_) => + } case _ => } } @@ -851,7 +886,7 @@ class NetworkClient( } } try Terminal.console.withRawSystemIn(read()) - catch { case _: InterruptedException | _: ClosedChannelException => stopped.set(true) } + catch { case NonFatal(_) => stopped.set(true) } } def drain(): Unit = inLock.synchronized { @@ -897,20 +932,18 @@ object NetworkClient { override def success(msg: String): Unit = appender.success(msg) } } - private def simpleConsoleInterface(printStream: PrintStream): ConsoleInterface = + private def simpleConsoleInterface(doPrintln: String => Unit): ConsoleInterface = new ConsoleInterface { import scala.Console.{ GREEN, RED, RESET, YELLOW } - override def appendLog(level: Level.Value, message: => String): Unit = { + override def appendLog(level: Level.Value, message: => String): Unit = synchronized { val prefix = level match { case Level.Error => s"[$RED$level$RESET]" case Level.Warn => s"[$YELLOW$level$RESET]" case _ => s"[$RESET$level$RESET]" } - message.split("\n").foreach { line => - if (!line.trim.isEmpty) printStream.println(s"$prefix $line") - } + message.linesIterator.foreach(line => doPrintln(s"$prefix $line")) } - override def success(msg: String): Unit = printStream.println(s"[${GREEN}success$RESET] $msg") + override def success(msg: String): Unit = doPrintln(s"[${GREEN}success$RESET] $msg") } private[client] class Arguments( val baseDirectory: File, @@ -961,8 +994,29 @@ object NetworkClient { baseDirectory: File, args: Array[String], inputStream: InputStream, - errorStream: PrintStream, printStream: PrintStream, + errorStream: PrintStream, + useJNI: Boolean + ): Int = { + val client = + simpleClient( + NetworkClient.parseArgs(args).withBaseDirectory(baseDirectory), + inputStream, + printStream, + errorStream, + useJNI, + ) + try { + if (client.connect(log = true, promptCompleteUsers = false)) client.run() + else 1 + } catch { case _: Exception => 1 } finally client.close() + } + def client( + baseDirectory: File, + args: Array[String], + inputStream: InputStream, + errorStream: PrintStream, + terminal: Terminal, useJNI: Boolean ): Int = { val client = @@ -970,8 +1024,8 @@ object NetworkClient { NetworkClient.parseArgs(args).withBaseDirectory(baseDirectory), inputStream, errorStream, - printStream, useJNI, + terminal ) try { if (client.connect(log = true, promptCompleteUsers = false)) client.run() @@ -982,17 +1036,27 @@ object NetworkClient { arguments: Arguments, inputStream: InputStream, errorStream: PrintStream, - printStream: PrintStream, useJNI: Boolean, - ): NetworkClient = - new NetworkClient( - arguments, - NetworkClient.simpleConsoleInterface(printStream), - inputStream, - errorStream, - printStream, - useJNI, - ) + terminal: Terminal + ): NetworkClient = { + val doPrint: String => Unit = line => { + if (terminal.getLastLine.isDefined) terminal.printStream.println() + terminal.printStream.println(line) + } + val interface = NetworkClient.simpleConsoleInterface(doPrint) + val printStream = terminal.printStream + new NetworkClient(arguments, interface, inputStream, errorStream, printStream, useJNI) + } + private def simpleClient( + arguments: Arguments, + inputStream: InputStream, + printStream: PrintStream, + errorStream: PrintStream, + useJNI: Boolean, + ): NetworkClient = { + val interface = NetworkClient.simpleConsoleInterface(printStream.println) + new NetworkClient(arguments, interface, inputStream, errorStream, printStream, useJNI) + } def main(args: Array[String]): Unit = { val (jnaArg, restOfArgs) = args.partition(_ == "--jna") val useJNI = jnaArg.isEmpty @@ -1005,8 +1069,9 @@ object NetworkClient { System.out.flush() }) Runtime.getRuntime.addShutdownHook(hook) - System.exit(Terminal.withStreams { - try client(base, restOfArgs, System.in, System.err, System.out, useJNI) + System.exit(Terminal.withStreams(false) { + val term = Terminal.console + try client(base, restOfArgs, term.inputStream, System.err, term, useJNI) finally { Runtime.getRuntime.removeShutdownHook(hook) hook.run() diff --git a/main-command/src/main/scala/sbt/internal/ui/UITask.scala b/main-command/src/main/scala/sbt/internal/ui/UITask.scala index 64e4a8439..0b5618cd2 100644 --- a/main-command/src/main/scala/sbt/internal/ui/UITask.scala +++ b/main-command/src/main/scala/sbt/internal/ui/UITask.scala @@ -11,14 +11,14 @@ import java.io.File import java.nio.channels.ClosedChannelException import java.util.concurrent.atomic.AtomicBoolean -import jline.console.history.PersistentHistory +//import jline.console.history.PersistentHistory import sbt.BasicCommandStrings.{ Cancel, TerminateAction, Shutdown } import sbt.BasicKeys.{ historyPath, terminalShellPrompt } import sbt.State import sbt.internal.CommandChannel import sbt.internal.util.ConsoleAppender.{ ClearPromptLine, ClearScreenAfterCursor, DeleteLine } import sbt.internal.util._ -import sbt.internal.util.complete.{ JLineCompletion, Parser } +import sbt.internal.util.complete.{ Parser } import scala.annotation.tailrec @@ -47,44 +47,31 @@ private[sbt] object UITask { def terminalReader(parser: Parser[_])( terminal: Terminal, state: State - ): Reader = { - val lineReader = LineReader.createReader(history(state), terminal, terminal.prompt) - JLineCompletion.installCustomCompletor(lineReader, parser) - () => { + ): Reader = { () => + try { val clear = terminal.ansi(ClearPromptLine, "") - try { - @tailrec def impl(): Either[String, String] = { - lineReader.readLine(clear + terminal.prompt.mkPrompt()) match { - case null if terminal == Terminal.console && System.console == null => - // No stdin is attached to the process so just ignore the result and - // block until the thread is interrupted. - this.synchronized(this.wait()) - Right("") // should be unreachable - // JLine returns null on ctrl+d when there is no other input. This interprets - // ctrl+d with no imput as an exit - case null => Left(TerminateAction) - case s: String => - lineReader.getHistory match { - case p: PersistentHistory => - p.add(s) - p.flush() - case _ => - } - s match { - case "" => impl() - case cmd @ (`Shutdown` | `TerminateAction` | `Cancel`) => Left(cmd) - case cmd => - if (terminal.prompt != Prompt.Batch) terminal.setPrompt(Prompt.Running) - terminal.printStream.write(Int.MinValue) - Right(cmd) - } - } + @tailrec def impl(): Either[String, String] = { + val reader = LineReader.createReader(history(state), parser, terminal, terminal.prompt) + (try reader.readLine(clear + terminal.prompt.mkPrompt()) + finally reader.close) match { + case None if terminal == Terminal.console && System.console == null => + // No stdin is attached to the process so just ignore the result and + // block until the thread is interrupted. + this.synchronized(this.wait()) + Right("") // should be unreachable + // JLine returns null on ctrl+d when there is no other input. This interprets + // ctrl+d with no imput as an exit + case None => Left(TerminateAction) + case Some(s: String) => + s.trim() match { + case "" => impl() + case cmd @ (`Shutdown` | `TerminateAction` | `Cancel`) => Left(cmd) + case cmd => Right(cmd) + } } - impl() - } catch { - case _: InterruptedException => Right("") - } finally lineReader.close() - } + } + impl() + } catch { case e: InterruptedException => Right("") } } } private[this] def history(s: State): Option[File] = diff --git a/main/src/main/java/sbt/internal/MetaBuildLoader.java b/main/src/main/java/sbt/internal/MetaBuildLoader.java index ca67b1286..a027314b1 100644 --- a/main/src/main/java/sbt/internal/MetaBuildLoader.java +++ b/main/src/main/java/sbt/internal/MetaBuildLoader.java @@ -61,7 +61,7 @@ public final class MetaBuildLoader extends URLClassLoader { */ public static MetaBuildLoader makeLoader(final AppProvider appProvider) throws IOException { final Pattern pattern = - Pattern.compile("(test-interface-[0-9.]+|jline-[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar"); + Pattern.compile("^(test-interface-[0-9.]+|jline-[0-9.]+-sbt-.*|jansi-[0-9.]+)\\.jar"); final File[] cp = appProvider.mainClasspath(); final URL[] interfaceURLs = new URL[3]; final File[] extra = diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index 2b3dd1e8a..edec7feb3 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -1504,13 +1504,13 @@ object Defaults extends BuildCommon { def print(st: String) = { scala.Console.out.print(st); scala.Console.out.flush() } print(s) Terminal.get.withRawSystemIn { - Terminal.get.inputStream.read match { - case -1 => None + try Terminal.get.inputStream.read match { + case -1 | -2 => None case b => val res = b.toChar.toString println(res) Some(res) - } + } catch { case e: InterruptedException => None } } }), classes diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index ea02165d1..47764e91a 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -78,7 +78,7 @@ private[sbt] object xMain { BspClient.run(dealiasBaseDirectory(configuration)) } else { bootServerSocket.foreach(l => Terminal.setBootStreams(l.inputStream, l.outputStream)) - Terminal.withStreams { + Terminal.withStreams(true) { if (clientModByEnv || userCommands.exists(isClient)) { val args = userCommands.toList.filterNot(isClient) NetworkClient.run(dealiasBaseDirectory(configuration), args) diff --git a/main/src/main/scala/sbt/internal/CommandExchange.scala b/main/src/main/scala/sbt/internal/CommandExchange.scala index 6347c3f5d..ab169877d 100644 --- a/main/src/main/scala/sbt/internal/CommandExchange.scala +++ b/main/src/main/scala/sbt/internal/CommandExchange.scala @@ -133,10 +133,17 @@ private[sbt] final class CommandExchange { } } // Do not manually run GC until the user has been idling for at least the min gc interval. - impl(interval match { + val exec = impl(interval match { case d: FiniteDuration => Some(d.fromNow) case _ => None }, idleDeadline) + exec.source.foreach { s => + channelForName(s.channelName).foreach { + case c if c.terminal.prompt != Prompt.Batch => c.terminal.setPrompt(Prompt.Running) + case _ => + } + } + exec } private def addConsoleChannel(): Unit = @@ -412,6 +419,10 @@ private[sbt] final class CommandExchange { case _ => } case _ => + channels.foreach { + case nc: NetworkChannel => nc.shutdown(true, Some(("", ""))) + case c => c.shutdown(false) + } } private[sbt] def shutdown(name: String): Unit = { @@ -448,7 +459,9 @@ private[sbt] final class CommandExchange { case mt: FastTrackTask => mt.task match { case `attach` => mt.channel.prompt(ConsolePromptEvent(lastState.get)) - case `Cancel` => Option(currentExecRef.get).foreach(cancel) + case `Cancel` => + Option(currentExecRef.get).foreach(cancel) + mt.channel.prompt(ConsolePromptEvent(lastState.get)) case t if t.startsWith(ContinuousCommands.stopWatch) => ContinuousCommands.stopWatchImpl(mt.channel.name) mt.channel match { @@ -458,6 +471,10 @@ private[sbt] final class CommandExchange { commandQueue.add(Exec(t, None, None)) case `TerminateAction` => exit(mt) case `Shutdown` => + val console = Terminal.console + val needNewLine = console.prompt.isInstanceOf[Prompt.AskUser] + console.setPrompt(Prompt.Batch) + if (needNewLine) console.printStream.println() channels.find(_.name == mt.channel.name) match { case Some(c: NetworkChannel) => c.shutdown(false) case _ => diff --git a/main/src/main/scala/sbt/internal/Continuous.scala b/main/src/main/scala/sbt/internal/Continuous.scala index ce2c1d6b1..e52a041a8 100644 --- a/main/src/main/scala/sbt/internal/Continuous.scala +++ b/main/src/main/scala/sbt/internal/Continuous.scala @@ -1217,7 +1217,6 @@ private[sbt] object ContinuousCommands { ) extends Thread(s"sbt-${channel.name}-watch-ui-thread") with UITask { override private[sbt] def reader: UITask.Reader = () => { - channel.terminal.printStream.write(Int.MinValue) def stop = Right(s"${ContinuousCommands.stopWatch} ${channel.name}") val exitAction: Watch.Action = { Watch.apply( diff --git a/main/src/main/scala/sbt/internal/TaskProgress.scala b/main/src/main/scala/sbt/internal/TaskProgress.scala index ebdd06f19..ed74d20b1 100644 --- a/main/src/main/scala/sbt/internal/TaskProgress.scala +++ b/main/src/main/scala/sbt/internal/TaskProgress.scala @@ -53,11 +53,12 @@ private[sbt] class TaskProgress private () if (firstTime.compareAndSet(true, activeExceedingThreshold.isEmpty)) threshold else sleepDuration val limit = duration.fromNow - while (Deadline.now < limit) { + while (Deadline.now < limit && !isClosed.get && active.nonEmpty) { var task = tasks.poll((limit - Deadline.now).toMillis, TimeUnit.MILLISECONDS) while (task != null) { if (containsSkipTasks(Vector(task)) || lastTaskCount.get == 0) doReport() task = tasks.poll + tasks.clear() } } } catch { diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 6a9860503..828beeab3 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -99,7 +99,6 @@ final class NetworkChannel( addFastTrackTask(attach) } private[sbt] def prompt(): Unit = { - terminal.setPrompt(Prompt.Running) interactive.set(true) jsonRpcNotify(promptChannel, "") } @@ -641,7 +640,7 @@ final class NetworkChannel( case -1 => throw new ClosedChannelException() case b => b } - } catch { case _: IOException => -1 } + } catch { case e: IOException => -1 } } override def available(): Int = inputBuffer.size } @@ -774,25 +773,82 @@ final class NetworkChannel( Some(result(queue.take)) } } - override def getBooleanCapability(capability: String): Boolean = + override def getBooleanCapability(capability: String, jline3: Boolean): Boolean = getCapability( - TerminalCapabilitiesQuery(boolean = Some(capability), numeric = None, string = None), + TerminalCapabilitiesQuery( + boolean = Some(capability), + numeric = None, + string = None, + jline3 + ), _.boolean.getOrElse(false) ).getOrElse(false) - override def getNumericCapability(capability: String): Int = + override def getNumericCapability(capability: String, jline3: Boolean): Integer = getCapability( - TerminalCapabilitiesQuery(boolean = None, numeric = Some(capability), string = None), - _.numeric.getOrElse(-1) - ).getOrElse(-1) - override def getStringCapability(capability: String): String = + TerminalCapabilitiesQuery( + boolean = None, + numeric = Some(capability), + string = None, + jline3 + ), + (_: TerminalCapabilitiesResponse).numeric.map(Integer.valueOf).getOrElse(-1: Integer) + ).getOrElse(-1: Integer) + override def getStringCapability(capability: String, jline3: Boolean): String = getCapability( - TerminalCapabilitiesQuery(boolean = None, numeric = None, string = Some(capability)), + TerminalCapabilitiesQuery( + boolean = None, + numeric = None, + string = Some(capability), + jline3 + ), _.string.flatMap { case "null" => None case s => Some(s) }.orNull ).getOrElse("") + override private[sbt] def getAttributes: Map[String, String] = + if (closed.get) Map.empty + else { + import sbt.protocol.codec.JsonProtocol._ + val queue = VirtualTerminal.sendTerminalAttributesQuery( + name, + jsonRpcRequest + ) + try { + val a = queue.take + Map( + "iflag" -> a.iflag, + "oflag" -> a.oflag, + "cflag" -> a.cflag, + "lflag" -> a.lflag, + "cchars" -> a.cchars + ) + } catch { case _: InterruptedException => Map.empty } + } + override private[sbt] def setAttributes(attributes: Map[String, String]): Unit = + if (!closed.get) { + import sbt.protocol.codec.JsonProtocol._ + val attrs = TerminalSetAttributesCommand( + iflag = attributes.getOrElse("iflag", ""), + oflag = attributes.getOrElse("oflag", ""), + cflag = attributes.getOrElse("cflag", ""), + lflag = attributes.getOrElse("lflag", ""), + cchars = attributes.getOrElse("cchars", ""), + ) + val queue = VirtualTerminal.setTerminalAttributes(name, jsonRpcRequest, attrs) + try queue.take + catch { case _: InterruptedException => } + } + override def setSize(width: Int, height: Int): Unit = + if (!closed.get) { + import sbt.protocol.codec.JsonProtocol._ + val size = TerminalSetSizeCommand(width, height) + val queue = VirtualTerminal.setTerminalSize(name, jsonRpcRequest, size) + try queue.take + catch { case _: InterruptedException => } + } + override def toString: String = s"NetworkTerminal($name)" override def close(): Unit = if (closed.compareAndSet(false, true)) { val threads = blockedThreads.synchronized { diff --git a/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala b/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala index 55659ef61..c299c0f4c 100644 --- a/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala +++ b/main/src/main/scala/sbt/internal/server/VirtualTerminal.scala @@ -25,16 +25,27 @@ import sbt.protocol.Serialization.{ import sjsonnew.support.scalajson.unsafe.Converter import sbt.protocol.{ Attach, + TerminalAttributesQuery, + TerminalAttributesResponse, TerminalCapabilitiesQuery, TerminalCapabilitiesResponse, - TerminalPropertiesResponse + TerminalPropertiesResponse, + TerminalSetAttributesCommand, + TerminalSetSizeCommand, } +import sbt.protocol.codec.JsonProtocol._ object VirtualTerminal { private[this] val pendingTerminalProperties = new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalPropertiesResponse]]() private[this] val pendingTerminalCapabilities = new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalCapabilitiesResponse]] + private[this] val pendingTerminalAttributes = + new ConcurrentHashMap[(String, String), ArrayBlockingQueue[TerminalAttributesResponse]] + private[this] val pendingTerminalSetAttributes = + new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]] + private[this] val pendingTerminalSetSize = + new ConcurrentHashMap[(String, String), ArrayBlockingQueue[Unit]] private[sbt] def sendTerminalPropertiesQuery( channelName: String, jsonRpcRequest: (String, String, String) => Unit @@ -70,6 +81,39 @@ object VirtualTerminal { case _ => } } + private[sbt] def sendTerminalAttributesQuery( + channelName: String, + jsonRpcRequest: (String, String, TerminalAttributesQuery) => Unit, + ): ArrayBlockingQueue[TerminalAttributesResponse] = { + val id = UUID.randomUUID.toString + val queue = new ArrayBlockingQueue[TerminalAttributesResponse](1) + pendingTerminalAttributes.put((channelName, id), queue) + jsonRpcRequest(id, terminalCapabilities, TerminalAttributesQuery()) + queue + } + private[sbt] def setTerminalAttributes( + channelName: String, + jsonRpcRequest: (String, String, TerminalSetAttributesCommand) => Unit, + query: TerminalSetAttributesCommand + ): ArrayBlockingQueue[Unit] = { + val id = UUID.randomUUID.toString + val queue = new ArrayBlockingQueue[Unit](1) + pendingTerminalSetAttributes.put((channelName, id), queue) + jsonRpcRequest(id, terminalCapabilities, query) + queue + } + + private[sbt] def setTerminalSize( + channelName: String, + jsonRpcRequest: (String, String, TerminalSetSizeCommand) => Unit, + query: TerminalSetSizeCommand + ): ArrayBlockingQueue[Unit] = { + val id = UUID.randomUUID.toString + val queue = new ArrayBlockingQueue[Unit](1) + pendingTerminalSetSize.put((channelName, id), queue) + jsonRpcRequest(id, terminalCapabilities, query) + queue + } val handler = ServerHandler { cb => ServerIntent(requestHandler(cb), responseHandler(cb), notificationHandler(cb)) } @@ -77,7 +121,6 @@ object VirtualTerminal { private val requestHandler: Handler[JsonRpcRequestMessage] = callback => { case r if r.method == attach => - import sbt.protocol.codec.JsonProtocol.AttachFormat val isInteractive = r.params .flatMap(Converter.fromJson[Attach](_).toOption.map(_.interactive)) .exists(identity) @@ -89,7 +132,6 @@ object VirtualTerminal { private val responseHandler: Handler[JsonRpcResponseMessage] = callback => { case r if pendingTerminalProperties.get((callback.name, r.id)) != null => - import sbt.protocol.codec.JsonProtocol._ val response = r.result.flatMap(Converter.fromJson[TerminalPropertiesResponse](_).toOption) pendingTerminalProperties.remove((callback.name, r.id)) match { @@ -97,7 +139,6 @@ object VirtualTerminal { case buffer => response.foreach(buffer.put) } case r if pendingTerminalCapabilities.get((callback.name, r.id)) != null => - import sbt.protocol.codec.JsonProtocol._ val response = r.result.flatMap( Converter.fromJson[TerminalCapabilitiesResponse](_).toOption @@ -107,6 +148,24 @@ object VirtualTerminal { case buffer => buffer.put(response.getOrElse(TerminalCapabilitiesResponse(None, None, None))) } + case r if pendingTerminalAttributes.get((callback.name, r.id)) != null => + val response = + r.result.flatMap(Converter.fromJson[TerminalAttributesResponse](_).toOption) + pendingTerminalAttributes.remove((callback.name, r.id)) match { + case null => + case buffer => + buffer.put(response.getOrElse(TerminalAttributesResponse("", "", "", "", ""))) + } + case r if pendingTerminalSetAttributes.get((callback.name, r.id)) != null => + pendingTerminalSetAttributes.remove((callback.name, r.id)) match { + case null => + case buffer => buffer.put(()) + } + case r if pendingTerminalSetSize.get((callback.name, r.id)) != null => + pendingTerminalSetSize.remove((callback.name, r.id)) match { + case null => + case buffer => buffer.put(()) + } } private val notificationHandler: Handler[JsonRpcNotificationMessage] = callback => { diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 028368816..5cd96f44c 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -84,7 +84,9 @@ object Dependencies { val sjsonNewMurmurhash = sjsonNew("sjson-new-murmurhash") val jline = "org.scala-sbt.jline" % "jline" % "2.14.7-sbt-5e51b9d4f9631ebfa29753ce4accc57808e7fd6b" - val jansi = "org.fusesource.jansi" % "jansi" % "1.12" + val jline3 = "org.jline" % "jline" % "3.15.0" + val jline3Jansi = "org.jline" % "jline-terminal-jansi" % "3.15.0" + val jansi = "org.fusesource.jansi" % "jansi" % "1.18" val scalatest = "org.scalatest" %% "scalatest" % "3.0.8" val scalacheck = "org.scalacheck" %% "scalacheck" % "1.14.0" val specs2 = "org.specs2" %% "specs2-junit" % "4.0.1" diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalAttributesQuery.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalAttributesQuery.scala new file mode 100644 index 000000000..cfff9b3e1 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalAttributesQuery.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class TerminalAttributesQuery private () extends sbt.protocol.CommandMessage() with Serializable { + + + +override def equals(o: Any): Boolean = o match { + case _: TerminalAttributesQuery => true + case _ => false +} +override def hashCode: Int = { + 37 * (17 + "sbt.protocol.TerminalAttributesQuery".##) +} +override def toString: String = { + "TerminalAttributesQuery()" +} +private[this] def copy(): TerminalAttributesQuery = { + new TerminalAttributesQuery() +} + +} +object TerminalAttributesQuery { + + def apply(): TerminalAttributesQuery = new TerminalAttributesQuery() +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalAttributesResponse.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalAttributesResponse.scala new file mode 100644 index 000000000..61fd49968 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalAttributesResponse.scala @@ -0,0 +1,48 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class TerminalAttributesResponse private ( + val iflag: String, + val oflag: String, + val cflag: String, + val lflag: String, + val cchars: String) extends sbt.protocol.EventMessage() with Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TerminalAttributesResponse => (this.iflag == x.iflag) && (this.oflag == x.oflag) && (this.cflag == x.cflag) && (this.lflag == x.lflag) && (this.cchars == x.cchars) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalAttributesResponse".##) + iflag.##) + oflag.##) + cflag.##) + lflag.##) + cchars.##) + } + override def toString: String = { + "TerminalAttributesResponse(" + iflag + ", " + oflag + ", " + cflag + ", " + lflag + ", " + cchars + ")" + } + private[this] def copy(iflag: String = iflag, oflag: String = oflag, cflag: String = cflag, lflag: String = lflag, cchars: String = cchars): TerminalAttributesResponse = { + new TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars) + } + def withIflag(iflag: String): TerminalAttributesResponse = { + copy(iflag = iflag) + } + def withOflag(oflag: String): TerminalAttributesResponse = { + copy(oflag = oflag) + } + def withCflag(cflag: String): TerminalAttributesResponse = { + copy(cflag = cflag) + } + def withLflag(lflag: String): TerminalAttributesResponse = { + copy(lflag = lflag) + } + def withCchars(cchars: String): TerminalAttributesResponse = { + copy(cchars = cchars) + } +} +object TerminalAttributesResponse { + + def apply(iflag: String, oflag: String, cflag: String, lflag: String, cchars: String): TerminalAttributesResponse = new TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesQuery.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesQuery.scala index 2e270924c..1e6db3d04 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesQuery.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalCapabilitiesQuery.scala @@ -7,22 +7,23 @@ package sbt.protocol final class TerminalCapabilitiesQuery private ( val boolean: Option[String], val numeric: Option[String], - val string: Option[String]) extends sbt.protocol.CommandMessage() with Serializable { + val string: Option[String], + val jline3: Boolean) extends sbt.protocol.CommandMessage() with Serializable { override def equals(o: Any): Boolean = o match { - case x: TerminalCapabilitiesQuery => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string) + case x: TerminalCapabilitiesQuery => (this.boolean == x.boolean) && (this.numeric == x.numeric) && (this.string == x.string) && (this.jline3 == x.jline3) case _ => false } override def hashCode: Int = { - 37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesQuery".##) + boolean.##) + numeric.##) + string.##) + 37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalCapabilitiesQuery".##) + boolean.##) + numeric.##) + string.##) + jline3.##) } override def toString: String = { - "TerminalCapabilitiesQuery(" + boolean + ", " + numeric + ", " + string + ")" + "TerminalCapabilitiesQuery(" + boolean + ", " + numeric + ", " + string + ", " + jline3 + ")" } - private[this] def copy(boolean: Option[String] = boolean, numeric: Option[String] = numeric, string: Option[String] = string): TerminalCapabilitiesQuery = { - new TerminalCapabilitiesQuery(boolean, numeric, string) + private[this] def copy(boolean: Option[String] = boolean, numeric: Option[String] = numeric, string: Option[String] = string, jline3: Boolean = jline3): TerminalCapabilitiesQuery = { + new TerminalCapabilitiesQuery(boolean, numeric, string, jline3) } def withBoolean(boolean: Option[String]): TerminalCapabilitiesQuery = { copy(boolean = boolean) @@ -42,9 +43,12 @@ final class TerminalCapabilitiesQuery private ( def withString(string: String): TerminalCapabilitiesQuery = { copy(string = Option(string)) } + def withJline3(jline3: Boolean): TerminalCapabilitiesQuery = { + copy(jline3 = jline3) + } } object TerminalCapabilitiesQuery { - def apply(boolean: Option[String], numeric: Option[String], string: Option[String]): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string) - def apply(boolean: String, numeric: String, string: String): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(string)) + def apply(boolean: Option[String], numeric: Option[String], string: Option[String], jline3: Boolean): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(boolean, numeric, string, jline3) + def apply(boolean: String, numeric: String, string: String, jline3: Boolean): TerminalCapabilitiesQuery = new TerminalCapabilitiesQuery(Option(boolean), Option(numeric), Option(string), jline3) } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetAttributesCommand.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetAttributesCommand.scala new file mode 100644 index 000000000..f22e10c0a --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetAttributesCommand.scala @@ -0,0 +1,48 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class TerminalSetAttributesCommand private ( + val iflag: String, + val oflag: String, + val cflag: String, + val lflag: String, + val cchars: String) extends sbt.protocol.CommandMessage() with Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TerminalSetAttributesCommand => (this.iflag == x.iflag) && (this.oflag == x.oflag) && (this.cflag == x.cflag) && (this.lflag == x.lflag) && (this.cchars == x.cchars) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (37 * (37 * (37 * (17 + "sbt.protocol.TerminalSetAttributesCommand".##) + iflag.##) + oflag.##) + cflag.##) + lflag.##) + cchars.##) + } + override def toString: String = { + "TerminalSetAttributesCommand(" + iflag + ", " + oflag + ", " + cflag + ", " + lflag + ", " + cchars + ")" + } + private[this] def copy(iflag: String = iflag, oflag: String = oflag, cflag: String = cflag, lflag: String = lflag, cchars: String = cchars): TerminalSetAttributesCommand = { + new TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars) + } + def withIflag(iflag: String): TerminalSetAttributesCommand = { + copy(iflag = iflag) + } + def withOflag(oflag: String): TerminalSetAttributesCommand = { + copy(oflag = oflag) + } + def withCflag(cflag: String): TerminalSetAttributesCommand = { + copy(cflag = cflag) + } + def withLflag(lflag: String): TerminalSetAttributesCommand = { + copy(lflag = lflag) + } + def withCchars(cchars: String): TerminalSetAttributesCommand = { + copy(cchars = cchars) + } +} +object TerminalSetAttributesCommand { + + def apply(iflag: String, oflag: String, cflag: String, lflag: String, cchars: String): TerminalSetAttributesCommand = new TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetAttributesResponse.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetAttributesResponse.scala new file mode 100644 index 000000000..bfa6ee41d --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetAttributesResponse.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class TerminalSetAttributesResponse private () extends sbt.protocol.EventMessage() with Serializable { + + + +override def equals(o: Any): Boolean = o match { + case _: TerminalSetAttributesResponse => true + case _ => false +} +override def hashCode: Int = { + 37 * (17 + "sbt.protocol.TerminalSetAttributesResponse".##) +} +override def toString: String = { + "TerminalSetAttributesResponse()" +} +private[this] def copy(): TerminalSetAttributesResponse = { + new TerminalSetAttributesResponse() +} + +} +object TerminalSetAttributesResponse { + + def apply(): TerminalSetAttributesResponse = new TerminalSetAttributesResponse() +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetSizeCommand.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetSizeCommand.scala new file mode 100644 index 000000000..11104fc94 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetSizeCommand.scala @@ -0,0 +1,36 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class TerminalSetSizeCommand private ( + val width: Int, + val height: Int) extends sbt.protocol.CommandMessage() with Serializable { + + + + override def equals(o: Any): Boolean = o match { + case x: TerminalSetSizeCommand => (this.width == x.width) && (this.height == x.height) + case _ => false + } + override def hashCode: Int = { + 37 * (37 * (37 * (17 + "sbt.protocol.TerminalSetSizeCommand".##) + width.##) + height.##) + } + override def toString: String = { + "TerminalSetSizeCommand(" + width + ", " + height + ")" + } + private[this] def copy(width: Int = width, height: Int = height): TerminalSetSizeCommand = { + new TerminalSetSizeCommand(width, height) + } + def withWidth(width: Int): TerminalSetSizeCommand = { + copy(width = width) + } + def withHeight(height: Int): TerminalSetSizeCommand = { + copy(height = height) + } +} +object TerminalSetSizeCommand { + + def apply(width: Int, height: Int): TerminalSetSizeCommand = new TerminalSetSizeCommand(width, height) +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetSizeResponse.scala b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetSizeResponse.scala new file mode 100644 index 000000000..118e2b121 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/TerminalSetSizeResponse.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol +final class TerminalSetSizeResponse private () extends sbt.protocol.EventMessage() with Serializable { + + + +override def equals(o: Any): Boolean = o match { + case _: TerminalSetSizeResponse => true + case _ => false +} +override def hashCode: Int = { + 37 * (17 + "sbt.protocol.TerminalSetSizeResponse".##) +} +override def toString: String = { + "TerminalSetSizeResponse()" +} +private[this] def copy(): TerminalSetSizeResponse = { + new TerminalSetSizeResponse() +} + +} +object TerminalSetSizeResponse { + + def apply(): TerminalSetSizeResponse = new TerminalSetSizeResponse() +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala index ee79ca457..1ecd02122 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/CommandMessageFormats.scala @@ -6,6 +6,6 @@ package sbt.protocol.codec import _root_.sjsonnew.JsonFormat -trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats => -implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat5[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery]("type") +trait CommandMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.InitCommandFormats with sbt.protocol.codec.ExecCommandFormats with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats with sbt.protocol.codec.TerminalSetAttributesCommandFormats with sbt.protocol.codec.TerminalAttributesQueryFormats with sbt.protocol.codec.TerminalSetSizeCommandFormats => +implicit lazy val CommandMessageFormat: JsonFormat[sbt.protocol.CommandMessage] = flatUnionFormat8[sbt.protocol.CommandMessage, sbt.protocol.InitCommand, sbt.protocol.ExecCommand, sbt.protocol.SettingQuery, sbt.protocol.Attach, sbt.protocol.TerminalCapabilitiesQuery, sbt.protocol.TerminalSetAttributesCommand, sbt.protocol.TerminalAttributesQuery, sbt.protocol.TerminalSetSizeCommand]("type") } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala index 2694b0078..5475a901b 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/EventMessageFormats.scala @@ -6,6 +6,6 @@ package sbt.protocol.codec import _root_.sjsonnew.JsonFormat -trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats with sbt.internal.util.codec.JValueFormats with sbt.protocol.codec.SettingQuerySuccessFormats with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats => -implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat7[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure, sbt.protocol.TerminalPropertiesResponse, sbt.protocol.TerminalCapabilitiesResponse]("type") +trait EventMessageFormats { self: sjsonnew.BasicJsonProtocol with sbt.protocol.codec.ChannelAcceptedEventFormats with sbt.protocol.codec.LogEventFormats with sbt.protocol.codec.ExecStatusEventFormats with sbt.internal.util.codec.JValueFormats with sbt.protocol.codec.SettingQuerySuccessFormats with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats with sbt.protocol.codec.TerminalSetAttributesResponseFormats with sbt.protocol.codec.TerminalAttributesResponseFormats with sbt.protocol.codec.TerminalSetSizeResponseFormats => +implicit lazy val EventMessageFormat: JsonFormat[sbt.protocol.EventMessage] = flatUnionFormat10[sbt.protocol.EventMessage, sbt.protocol.ChannelAcceptedEvent, sbt.protocol.LogEvent, sbt.protocol.ExecStatusEvent, sbt.protocol.SettingQuerySuccess, sbt.protocol.SettingQueryFailure, sbt.protocol.TerminalPropertiesResponse, sbt.protocol.TerminalCapabilitiesResponse, sbt.protocol.TerminalSetAttributesResponse, sbt.protocol.TerminalAttributesResponse, sbt.protocol.TerminalSetSizeResponse]("type") } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala index de4aba238..e3a6e2b99 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/JsonProtocol.scala @@ -10,6 +10,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.protocol.codec.SettingQueryFormats with sbt.protocol.codec.AttachFormats with sbt.protocol.codec.TerminalCapabilitiesQueryFormats + with sbt.protocol.codec.TerminalSetAttributesCommandFormats + with sbt.protocol.codec.TerminalAttributesQueryFormats + with sbt.protocol.codec.TerminalSetSizeCommandFormats with sbt.protocol.codec.CommandMessageFormats with sbt.protocol.codec.CompletionParamsFormats with sbt.protocol.codec.ChannelAcceptedEventFormats @@ -20,6 +23,9 @@ trait JsonProtocol extends sjsonnew.BasicJsonProtocol with sbt.protocol.codec.SettingQueryFailureFormats with sbt.protocol.codec.TerminalPropertiesResponseFormats with sbt.protocol.codec.TerminalCapabilitiesResponseFormats + with sbt.protocol.codec.TerminalSetAttributesResponseFormats + with sbt.protocol.codec.TerminalAttributesResponseFormats + with sbt.protocol.codec.TerminalSetSizeResponseFormats with sbt.protocol.codec.EventMessageFormats with sbt.protocol.codec.SettingQueryResponseFormats with sbt.protocol.codec.CompletionResponseFormats diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalAttributesQueryFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalAttributesQueryFormats.scala new file mode 100644 index 000000000..a91e08aa5 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalAttributesQueryFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TerminalAttributesQueryFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TerminalAttributesQueryFormat: JsonFormat[sbt.protocol.TerminalAttributesQuery] = new JsonFormat[sbt.protocol.TerminalAttributesQuery] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalAttributesQuery = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + + unbuilder.endObject() + sbt.protocol.TerminalAttributesQuery() + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.TerminalAttributesQuery, builder: Builder[J]): Unit = { + builder.beginObject() + + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalAttributesResponseFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalAttributesResponseFormats.scala new file mode 100644 index 000000000..e117c0389 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalAttributesResponseFormats.scala @@ -0,0 +1,35 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TerminalAttributesResponseFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TerminalAttributesResponseFormat: JsonFormat[sbt.protocol.TerminalAttributesResponse] = new JsonFormat[sbt.protocol.TerminalAttributesResponse] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalAttributesResponse = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val iflag = unbuilder.readField[String]("iflag") + val oflag = unbuilder.readField[String]("oflag") + val cflag = unbuilder.readField[String]("cflag") + val lflag = unbuilder.readField[String]("lflag") + val cchars = unbuilder.readField[String]("cchars") + unbuilder.endObject() + sbt.protocol.TerminalAttributesResponse(iflag, oflag, cflag, lflag, cchars) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.TerminalAttributesResponse, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("iflag", obj.iflag) + builder.addField("oflag", obj.oflag) + builder.addField("cflag", obj.cflag) + builder.addField("lflag", obj.lflag) + builder.addField("cchars", obj.cchars) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesQueryFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesQueryFormats.scala index a26886a46..a7ba0d270 100644 --- a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesQueryFormats.scala +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalCapabilitiesQueryFormats.scala @@ -14,8 +14,9 @@ implicit lazy val TerminalCapabilitiesQueryFormat: JsonFormat[sbt.protocol.Termi val boolean = unbuilder.readField[Option[String]]("boolean") val numeric = unbuilder.readField[Option[String]]("numeric") val string = unbuilder.readField[Option[String]]("string") + val jline3 = unbuilder.readField[Boolean]("jline3") unbuilder.endObject() - sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string) + sbt.protocol.TerminalCapabilitiesQuery(boolean, numeric, string, jline3) case None => deserializationError("Expected JsObject but found None") } @@ -25,6 +26,7 @@ implicit lazy val TerminalCapabilitiesQueryFormat: JsonFormat[sbt.protocol.Termi builder.addField("boolean", obj.boolean) builder.addField("numeric", obj.numeric) builder.addField("string", obj.string) + builder.addField("jline3", obj.jline3) builder.endObject() } } diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetAttributesCommandFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetAttributesCommandFormats.scala new file mode 100644 index 000000000..021c9c226 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetAttributesCommandFormats.scala @@ -0,0 +1,35 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TerminalSetAttributesCommandFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TerminalSetAttributesCommandFormat: JsonFormat[sbt.protocol.TerminalSetAttributesCommand] = new JsonFormat[sbt.protocol.TerminalSetAttributesCommand] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetAttributesCommand = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val iflag = unbuilder.readField[String]("iflag") + val oflag = unbuilder.readField[String]("oflag") + val cflag = unbuilder.readField[String]("cflag") + val lflag = unbuilder.readField[String]("lflag") + val cchars = unbuilder.readField[String]("cchars") + unbuilder.endObject() + sbt.protocol.TerminalSetAttributesCommand(iflag, oflag, cflag, lflag, cchars) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.TerminalSetAttributesCommand, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("iflag", obj.iflag) + builder.addField("oflag", obj.oflag) + builder.addField("cflag", obj.cflag) + builder.addField("lflag", obj.lflag) + builder.addField("cchars", obj.cchars) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetAttributesResponseFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetAttributesResponseFormats.scala new file mode 100644 index 000000000..af4f3611a --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetAttributesResponseFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TerminalSetAttributesResponseFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TerminalSetAttributesResponseFormat: JsonFormat[sbt.protocol.TerminalSetAttributesResponse] = new JsonFormat[sbt.protocol.TerminalSetAttributesResponse] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetAttributesResponse = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + + unbuilder.endObject() + sbt.protocol.TerminalSetAttributesResponse() + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.TerminalSetAttributesResponse, builder: Builder[J]): Unit = { + builder.beginObject() + + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetSizeCommandFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetSizeCommandFormats.scala new file mode 100644 index 000000000..56fc296a1 --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetSizeCommandFormats.scala @@ -0,0 +1,29 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TerminalSetSizeCommandFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TerminalSetSizeCommandFormat: JsonFormat[sbt.protocol.TerminalSetSizeCommand] = new JsonFormat[sbt.protocol.TerminalSetSizeCommand] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetSizeCommand = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + val width = unbuilder.readField[Int]("width") + val height = unbuilder.readField[Int]("height") + unbuilder.endObject() + sbt.protocol.TerminalSetSizeCommand(width, height) + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.TerminalSetSizeCommand, builder: Builder[J]): Unit = { + builder.beginObject() + builder.addField("width", obj.width) + builder.addField("height", obj.height) + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetSizeResponseFormats.scala b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetSizeResponseFormats.scala new file mode 100644 index 000000000..1da62ae6f --- /dev/null +++ b/protocol/src/main/contraband-scala/sbt/protocol/codec/TerminalSetSizeResponseFormats.scala @@ -0,0 +1,27 @@ +/** + * This code is generated using [[https://www.scala-sbt.org/contraband/ sbt-contraband]]. + */ + +// DO NOT EDIT MANUALLY +package sbt.protocol.codec +import _root_.sjsonnew.{ Unbuilder, Builder, JsonFormat, deserializationError } +trait TerminalSetSizeResponseFormats { self: sjsonnew.BasicJsonProtocol => +implicit lazy val TerminalSetSizeResponseFormat: JsonFormat[sbt.protocol.TerminalSetSizeResponse] = new JsonFormat[sbt.protocol.TerminalSetSizeResponse] { + override def read[J](__jsOpt: Option[J], unbuilder: Unbuilder[J]): sbt.protocol.TerminalSetSizeResponse = { + __jsOpt match { + case Some(__js) => + unbuilder.beginObject(__js) + + unbuilder.endObject() + sbt.protocol.TerminalSetSizeResponse() + case None => + deserializationError("Expected JsObject but found None") + } + } + override def write[J](obj: sbt.protocol.TerminalSetSizeResponse, builder: Builder[J]): Unit = { + builder.beginObject() + + builder.endObject() + } +} +} diff --git a/protocol/src/main/contraband/server.contra b/protocol/src/main/contraband/server.contra index 04ce0054a..2cde1ec82 100644 --- a/protocol/src/main/contraband/server.contra +++ b/protocol/src/main/contraband/server.contra @@ -85,22 +85,50 @@ type ExecutionEvent { } type TerminalPropertiesResponse implements EventMessage { - width: Int! - height: Int! - isAnsiSupported: Boolean! - isColorEnabled: Boolean! - isSupershellEnabled: Boolean! - isEchoEnabled: Boolean! + width: Int! + height: Int! + isAnsiSupported: Boolean! + isColorEnabled: Boolean! + isSupershellEnabled: Boolean! + isEchoEnabled: Boolean! } type TerminalCapabilitiesQuery implements CommandMessage { - boolean: String - numeric: String - string: String + boolean: String + numeric: String + string: String + jline3: Boolean! } type TerminalCapabilitiesResponse implements EventMessage { - boolean: Boolean - numeric: Int - string: String + boolean: Boolean + numeric: Int + string: String } + +type TerminalSetAttributesCommand implements CommandMessage { + iflag: String!, + oflag: String!, + cflag: String!, + lflag: String!, + cchars: String!, +} + +type TerminalSetAttributesResponse implements EventMessage {} + +type TerminalAttributesQuery implements CommandMessage {} + +type TerminalAttributesResponse implements EventMessage { + iflag: String!, + oflag: String!, + cflag: String!, + lflag: String!, + cchars: String!, +} + +type TerminalSetSizeCommand implements CommandMessage { + width: Int! + height: Int! +} + +type TerminalSetSizeResponse implements EventMessage {} diff --git a/protocol/src/main/scala/sbt/protocol/Serialization.scala b/protocol/src/main/scala/sbt/protocol/Serialization.scala index 21e981059..e9cbcf1e1 100644 --- a/protocol/src/main/scala/sbt/protocol/Serialization.scala +++ b/protocol/src/main/scala/sbt/protocol/Serialization.scala @@ -26,6 +26,7 @@ object Serialization { private[sbt] val VsCode = "application/vscode-jsonrpc; charset=utf-8" val systemIn = "sbt/systemIn" val systemOut = "sbt/systemOut" + val systemOutFlush = "sbt/systemOutFlush" val terminalPropertiesQuery = "sbt/terminalPropertiesQuery" val terminalPropertiesResponse = "sbt/terminalPropertiesResponse" val terminalCapabilities = "sbt/terminalCapabilities" @@ -34,6 +35,9 @@ object Serialization { val attachResponse = "sbt/attachResponse" val cancelRequest = "sbt/cancelRequest" val promptChannel = "sbt/promptChannel" + val setTerminalAttributes = "sbt/setTerminalAttributes" + val getTerminalAttributes = "sbt/getTerminalAttributes" + val setTerminalSize = "sbt/setTerminalSize" val CancelAll = "__CancelAll" @deprecated("unused", since = "1.4.0")