mirror of https://github.com/sbt/sbt.git
Redraw command prompt after network command
Presently if a server command comes in while in the shell, the client output can appear on the same line as the command prompt and the command prompt will not appear again until the user hits enter. This is a confusing ux. For example, if I start an sbt server and type the partial command "comp" and then start up a client and run the clean command followed by a compile, the output looks like: [info] sbt server started at local:///Users/ethanatkins/.sbt/1.0/server/51cfad3281b3a8a1820a/sock sbt:scala-compile> comp[info] new client connected: network-1 [success] Total time: 0 s, completed Dec 12, 2019, 7:23:24 PM [success] Total time: 0 s, completed Dec 12, 2019, 7:23:27 PM [success] Total time: 2 s, completed Dec 12, 2019, 7:23:31 PM Now, if I type "ile\n", I get: [info] sbt server started at local:///Users/ethanatkins/.sbt/1.0/server/51cfad3281b3a8a1820a/sock ile [success] Total time: 0 s, completed Dec 12, 2019, 7:23:34 PM sbt:scala-compile> Following the same set of inputs after this change, I get: [info] sbt server started at local:///Users/ethanatkins/.sbt/1.0/server/51cfad3281b3a8a1820a/sock sbt:scala-compile> comp [info] new client connected: network-1 [success] Total time: 0 s, completed Dec 12, 2019, 7:25:58 PM sbt:scala-compile> comp [success] Total time: 0 s, completed Dec 12, 2019, 7:26:14 PM sbt:scala-compile> comp [success] Total time: 1 s, completed Dec 12, 2019, 7:26:17 PM sbt:scala-compile> compile [success] Total time: 0 s, completed Dec 12, 2019, 7:26:19 PM sbt:scala-compile> To implement this change, I added the redraw() method to LineReader which is a wrapper around ConsoleReader.drawLine; ConsoleReader.flush(). We invoke LineReader.redraw whenever the ConsoleChannel receives a ConsolePromptEvent and there is a running thread. To prevent log lines from being appended to the prompt line, in the CommandExchange we print a newline character whenever a new command is received from the network or a network client connects and we believe that there is an active prompt.
This commit is contained in:
parent
08091d64c1
commit
9218d3c087
|
|
@ -33,6 +33,11 @@ abstract class JLine extends LineReader {
|
||||||
Option("")
|
Option("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override def redraw(): Unit = {
|
||||||
|
reader.drawLine()
|
||||||
|
reader.flush()
|
||||||
|
}
|
||||||
|
|
||||||
private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] =
|
private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] =
|
||||||
readLineWithHistory(prompt, mask) map { x =>
|
readLineWithHistory(prompt, mask) map { x =>
|
||||||
x.trim
|
x.trim
|
||||||
|
|
@ -169,6 +174,7 @@ private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration)
|
||||||
|
|
||||||
trait LineReader {
|
trait LineReader {
|
||||||
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
|
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
|
||||||
|
def redraw(): Unit = ()
|
||||||
}
|
}
|
||||||
|
|
||||||
final class FullReader(
|
final class FullReader(
|
||||||
|
|
|
||||||
|
|
@ -10,27 +10,29 @@ package internal
|
||||||
|
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.nio.channels.ClosedChannelException
|
import java.nio.channels.ClosedChannelException
|
||||||
|
import java.util.concurrent.atomic.AtomicReference
|
||||||
|
|
||||||
import sbt.BasicKeys._
|
import sbt.BasicKeys._
|
||||||
import sbt.internal.util.Util.AnyOps
|
|
||||||
import sbt.internal.util._
|
import sbt.internal.util._
|
||||||
import sbt.protocol.EventMessage
|
import sbt.protocol.EventMessage
|
||||||
import sjsonnew.JsonFormat
|
import sjsonnew.JsonFormat
|
||||||
|
|
||||||
private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel {
|
private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel {
|
||||||
private var askUserThread: Option[Thread] = None
|
private[this] val askUserThread = new AtomicReference[AskUserThread]
|
||||||
def makeAskUserThread(s: State): Thread = new Thread("ask-user-thread") {
|
private[this] def getPrompt(s: State): String = s.get(shellPrompt) match {
|
||||||
val history = (s get historyPath) getOrElse (new File(s.baseDir, ".history")).some
|
case Some(pf) => pf(s)
|
||||||
val prompt = (s get shellPrompt) match {
|
case None =>
|
||||||
case Some(pf) => pf(s)
|
def ansi(s: String): String = if (ConsoleAppender.formatEnabledInEnv) s"$s" else ""
|
||||||
case None =>
|
s"${ansi(ConsoleAppender.DeleteLine)}> ${ansi(ConsoleAppender.clearScreen(0))}"
|
||||||
def ansi(s: String): String = if (ConsoleAppender.formatEnabledInEnv) s"$s" else ""
|
}
|
||||||
s"${ansi(ConsoleAppender.DeleteLine)}> ${ansi(ConsoleAppender.clearScreen(0))}"
|
private[this] class AskUserThread(s: State) extends Thread("ask-user-thread") {
|
||||||
}
|
private val history = s.get(historyPath).getOrElse(Some(new File(s.baseDir, ".history")))
|
||||||
val reader =
|
private val prompt = getPrompt(s)
|
||||||
|
private val reader =
|
||||||
new FullReader(history, s.combinedParser, JLine.HandleCONT, Terminal.throwOnClosedSystemIn)
|
new FullReader(history, s.combinedParser, JLine.HandleCONT, Terminal.throwOnClosedSystemIn)
|
||||||
override def run(): Unit = {
|
setDaemon(true)
|
||||||
// This internally handles thread interruption and returns Some("")
|
start()
|
||||||
|
override def run(): Unit =
|
||||||
try {
|
try {
|
||||||
reader.readLine(prompt) match {
|
reader.readLine(prompt) match {
|
||||||
case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name))))
|
case Some(cmd) => append(Exec(cmd, Some(Exec.newExecId), Some(CommandSource(name))))
|
||||||
|
|
@ -38,12 +40,18 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel
|
||||||
println("") // Prevents server shutdown log lines from appearing on the prompt line
|
println("") // Prevents server shutdown log lines from appearing on the prompt line
|
||||||
append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name))))
|
append(Exec("exit", Some(Exec.newExecId), Some(CommandSource(name))))
|
||||||
}
|
}
|
||||||
|
()
|
||||||
} catch {
|
} catch {
|
||||||
case _: ClosedChannelException =>
|
case _: ClosedChannelException =>
|
||||||
}
|
} finally askUserThread.synchronized(askUserThread.set(null))
|
||||||
askUserThread = None
|
def redraw(): Unit = {
|
||||||
|
System.out.print(ConsoleAppender.clearLine(0))
|
||||||
|
reader.redraw()
|
||||||
|
System.out.print(ConsoleAppender.clearScreen(0))
|
||||||
|
System.out.flush()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
private[this] def makeAskUserThread(s: State): AskUserThread = new AskUserThread(s)
|
||||||
|
|
||||||
def run(s: State): State = s
|
def run(s: State): State = s
|
||||||
|
|
||||||
|
|
@ -54,21 +62,24 @@ private[sbt] final class ConsoleChannel(val name: String) extends CommandChannel
|
||||||
def publishEventMessage(event: EventMessage): Unit =
|
def publishEventMessage(event: EventMessage): Unit =
|
||||||
event match {
|
event match {
|
||||||
case e: ConsolePromptEvent =>
|
case e: ConsolePromptEvent =>
|
||||||
askUserThread match {
|
if (Terminal.systemInIsAttached) {
|
||||||
case Some(_) =>
|
askUserThread.synchronized {
|
||||||
case _ =>
|
askUserThread.get match {
|
||||||
val x = makeAskUserThread(e.state)
|
case null => askUserThread.set(makeAskUserThread(e.state))
|
||||||
askUserThread = Some(x)
|
case t => t.redraw()
|
||||||
x.start()
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case _ => //
|
case _ => //
|
||||||
}
|
}
|
||||||
|
|
||||||
def shutdown(): Unit =
|
def shutdown(): Unit = askUserThread.synchronized {
|
||||||
askUserThread match {
|
askUserThread.get match {
|
||||||
case Some(x) if x.isAlive =>
|
case null =>
|
||||||
x.interrupt()
|
case t if t.isAlive =>
|
||||||
askUserThread = None
|
t.interrupt()
|
||||||
|
askUserThread.set(null)
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import sbt.internal.protocol.JsonRpcResponseError
|
||||||
import sbt.internal.langserver.{ LogMessageParams, MessageType }
|
import sbt.internal.langserver.{ LogMessageParams, MessageType }
|
||||||
import sbt.internal.server._
|
import sbt.internal.server._
|
||||||
import sbt.internal.util.codec.JValueFormats
|
import sbt.internal.util.codec.JValueFormats
|
||||||
import sbt.internal.util.{ MainAppender, ObjectEvent, StringEvent }
|
import sbt.internal.util.{ ConsoleOut, MainAppender, ObjectEvent, StringEvent, Terminal }
|
||||||
import sbt.io.syntax._
|
import sbt.io.syntax._
|
||||||
import sbt.io.{ Hash, IO }
|
import sbt.io.{ Hash, IO }
|
||||||
import sbt.protocol.{ EventMessage, ExecStatusEvent }
|
import sbt.protocol.{ EventMessage, ExecStatusEvent }
|
||||||
|
|
@ -50,6 +50,7 @@ private[sbt] final class CommandExchange {
|
||||||
private val channelBufferLock = new AnyRef {}
|
private val channelBufferLock = new AnyRef {}
|
||||||
private val commandChannelQueue = new LinkedBlockingQueue[CommandChannel]
|
private val commandChannelQueue = new LinkedBlockingQueue[CommandChannel]
|
||||||
private val nextChannelId: AtomicInteger = new AtomicInteger(0)
|
private val nextChannelId: AtomicInteger = new AtomicInteger(0)
|
||||||
|
private[this] val activePrompt = new AtomicBoolean(false)
|
||||||
private lazy val jsonFormat = new sjsonnew.BasicJsonProtocol with JValueFormats {}
|
private lazy val jsonFormat = new sjsonnew.BasicJsonProtocol with JValueFormats {}
|
||||||
|
|
||||||
def channels: List[CommandChannel] = channelBuffer.toList
|
def channels: List[CommandChannel] = channelBuffer.toList
|
||||||
|
|
@ -83,7 +84,11 @@ private[sbt] final class CommandExchange {
|
||||||
commandChannelQueue.poll(1, TimeUnit.SECONDS)
|
commandChannelQueue.poll(1, TimeUnit.SECONDS)
|
||||||
slurpMessages()
|
slurpMessages()
|
||||||
Option(commandQueue.poll) match {
|
Option(commandQueue.poll) match {
|
||||||
case Some(x) => x
|
case Some(exec) =>
|
||||||
|
val needFinish = needToFinishPromptLine()
|
||||||
|
if (exec.source.fold(needFinish)(s => needFinish && s.channelName != "console0"))
|
||||||
|
ConsoleOut.systemOut.println("")
|
||||||
|
exec
|
||||||
case None =>
|
case None =>
|
||||||
val newDeadline = if (deadline.fold(false)(_.isOverdue())) {
|
val newDeadline = if (deadline.fold(false)(_.isOverdue())) {
|
||||||
GCUtil.forceGcWithInterval(interval, logger)
|
GCUtil.forceGcWithInterval(interval, logger)
|
||||||
|
|
@ -129,6 +134,7 @@ private[sbt] final class CommandExchange {
|
||||||
|
|
||||||
def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = {
|
def onIncomingSocket(socket: Socket, instance: ServerInstance): Unit = {
|
||||||
val name = newNetworkName
|
val name = newNetworkName
|
||||||
|
if (needToFinishPromptLine()) ConsoleOut.systemOut.println("")
|
||||||
s.log.info(s"new client connected: $name")
|
s.log.info(s"new client connected: $name")
|
||||||
val logger: Logger = {
|
val logger: Logger = {
|
||||||
val log = LogExchange.logger(name, None, None)
|
val log = LogExchange.logger(name, None, None)
|
||||||
|
|
@ -362,7 +368,9 @@ private[sbt] final class CommandExchange {
|
||||||
// Special treatment for ConsolePromptEvent since it's hand coded without codec.
|
// Special treatment for ConsolePromptEvent since it's hand coded without codec.
|
||||||
case entry: ConsolePromptEvent =>
|
case entry: ConsolePromptEvent =>
|
||||||
channels collect {
|
channels collect {
|
||||||
case c: ConsoleChannel => c.publishEventMessage(entry)
|
case c: ConsoleChannel =>
|
||||||
|
c.publishEventMessage(entry)
|
||||||
|
activePrompt.set(Terminal.systemInIsAttached)
|
||||||
}
|
}
|
||||||
case entry: ExecStatusEvent =>
|
case entry: ExecStatusEvent =>
|
||||||
channels collect {
|
channels collect {
|
||||||
|
|
@ -380,4 +388,5 @@ private[sbt] final class CommandExchange {
|
||||||
|
|
||||||
removeChannels(toDel.toList)
|
removeChannels(toDel.toList)
|
||||||
}
|
}
|
||||||
|
private[this] def needToFinishPromptLine(): Boolean = activePrompt.compareAndSet(true, false)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue