sbt.internal.util.LineReader.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of completion_2.12 Show documentation
Show all versions of completion_2.12 Show documentation
sbt is an interactive build tool
/*
* sbt
* Copyright 2023, Scala center
* Copyright 2011 - 2022, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
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 org.jline.utils.ClosedException
import sbt.internal.util.complete.Parser
import sbt.io.syntax._
import scala.annotation.tailrec
import scala.concurrent.duration._
import scala.util.control.NonFatal
import java.nio.channels.ClosedByInterruptException
import java.net.MalformedURLException
import org.jline.builtins.InputRC
trait LineReader extends AutoCloseable {
def readLine(prompt: String, mask: Option[Char] = None): Option[String]
override def close(): Unit = {}
}
object LineReader {
val HandleCONT =
!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) {
val cand = pl.line() match {
case line if line.endsWith(" ") => c.append
case line => line.split(" ").last + c.append
}
// https://github.com/jline/jline3/blob/9a4971868e4bdd29a36e454de01f54d3cd6071e0/reader/src/main/java/org/jline/reader/Candidate.java#L123-L131
// "If the candidate is complete and is selected, a space separator will be added."
val complete = false
candidates.add(new Candidate(cand, cand, null, null, null, null, complete))
}
}
}
}
private[this] def inputrcFileUrl(): Option[URL] = {
// keep jline2 compatibility
// https://github.com/jline/jline2/blob/12b98d94589e3bd6a6/src/main/java/jline/console/ConsoleReader.java#L291-L306
sys.props
.get("jline.inputrc")
.flatMap { path =>
try {
Some(url(path))
} catch {
case _: MalformedURLException =>
Some(file(path).toURI.toURL)
}
}
.orElse {
sys.props.get("user.home").map { home =>
val f = file(home) / ".inputrc"
(if (f.isFile) f else file("/etc/inputrc")).toURI.toURL
}
}
}
// cache on memory.
private[this] lazy val inputrcFileContents: Option[Array[Byte]] =
inputrcFileUrl().map(in => sbt.io.IO.readBytes(in.openStream()))
def createReader(
historyPath: Option[File],
parser: Parser[_],
terminal: Terminal,
): LineReader = {
// We may want to consider insourcing LineReader.java from jline. We don't otherwise
// directly need jline3 for sbt.
new LineReader {
override def readLine(prompt: String, mask: Option[Char]): Option[String] = {
val term = JLine3(terminal)
val reader = LineReaderBuilder.builder().terminal(term).completer(completer(parser)).build()
try {
inputrcFileContents.foreach { bytes =>
InputRC.configure(
reader,
new ByteArrayInputStream(bytes)
)
}
} catch {
case NonFatal(_) =>
// ignore
}
historyPath.foreach(f => reader.setVariable(JLineReader.HISTORY_FILE, f))
val signalRegistration = terminal match {
case _: Terminal.ConsoleTerminal => Some(Signals.register(() => terminal.write(-1)))
case _ => None
}
try terminal.withRawInput {
Option(mask.map(reader.readLine(prompt, _)).getOrElse(reader.readLine(prompt)))
} catch {
case e: EndOfFileException =>
if (terminal == Terminal.console && !Terminal.hasConsole) None
else Some("exit")
case _: IOError | _: ClosedException => Some("exit")
case _: UserInterruptException | _: ClosedByInterruptException |
_: UncheckedIOException =>
throw new InterruptedException
} finally {
signalRegistration.foreach(_.remove())
terminal.prompt.reset()
term.close()
}
}
}
}
def createJLine2Reader(
historyPath: Option[File],
terminal: Terminal,
prompt: Prompt = Prompt.Running,
): ConsoleReader = {
val cr = Terminal.createReader(terminal, prompt)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
case None => new MemoryHistory
case Some(file) => new FileHistory(file): MemoryHistory
}
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr.setHistoryEnabled(true)
cr
}
def simple(terminal: Terminal): LineReader = new SimpleReader(None, HandleCONT, terminal)
def simple(
historyPath: Option[File],
handleCONT: Boolean = HandleCONT,
injectThreadSleep: Boolean = false
): LineReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep)
}
abstract class JLine extends LineReader {
protected[this] def handleCONT: Boolean
protected[this] def reader: ConsoleReader
@deprecated("For binary compatibility only", "1.4.0")
protected[this] def injectThreadSleep: Boolean = false
@deprecated("For binary compatibility only", "1.4.0")
protected[this] lazy val in: InputStream = Terminal.wrappedSystemIn
override def readLine(prompt: String, mask: Option[Char] = None): Option[String] =
unsynchronizedReadLine(prompt, mask)
private[this] def unsynchronizedReadLine(prompt: String, mask: Option[Char]): Option[String] =
readLineWithHistory(prompt, mask) map { x =>
x.trim
}
private[this] def readLineWithHistory(prompt: String, mask: Option[Char]): Option[String] =
reader.getHistory match {
case fh: FileHistory =>
try readLineDirect(prompt, mask)
finally fh.flush()
case _ => readLineDirect(prompt, mask)
}
private[this] def readLineDirect(prompt: String, mask: Option[Char]): Option[String] =
if (handleCONT)
Signals.withHandler(() => resume(), signal = Signals.CONT)(
() => readLineDirectRaw(prompt, mask)
)
else
readLineDirectRaw(prompt, mask)
private[this] def readLineDirectRaw(prompt: String, mask: Option[Char]): Option[String] = {
val newprompt = handleMultilinePrompt(prompt)
mask match {
case Some(m) => Option(reader.readLine(newprompt, m))
case None => Option(reader.readLine(newprompt))
}
}
private[this] def handleMultilinePrompt(prompt: String): String = {
val lines0 = """\r?\n""".r.split(prompt)
lines0.length match {
case 0 | 1 => handleProgress(prompt)
case _ =>
val lines = lines0.toList map handleProgress
// Workaround for regression jline/jline2#205
reader.getOutput.write(lines.init.mkString("\n") + "\n")
lines.last
}
}
private[this] def handleProgress(prompt: String): String = {
import ConsoleAppender._
if (showProgress) s"$DeleteLine" + prompt
else prompt
}
private[this] def resume(): Unit = {
Terminal.reset()
reader.drawLine()
reader.flush()
}
}
@deprecated("Use LineReader apis", "1.4.0")
private[sbt] object JLine {
@deprecated("For binary compatibility only", "1.4.0")
protected[this] val originalIn = new FileInputStream(FileDescriptor.in)
@deprecated("Handled by Terminal.fixTerminalProperty", "1.4.0")
private[sbt] def fixTerminalProperty(): Unit = ()
@deprecated("For binary compatibility only", "1.4.0")
private[sbt] def makeInputStream(injectThreadSleep: Boolean): InputStream =
if (injectThreadSleep) new InputStreamWrapper(originalIn, 2.milliseconds)
else originalIn
// When calling this, ensure that enableEcho has been or will be called.
// TerminalFactory.get will initialize the terminal to disable echo.
@deprecated("Don't use jline.Terminal directly", "1.4.0")
private[sbt] def terminal: jline.Terminal = Terminal.deprecatedTeminal
/**
* For accessing the JLine Terminal object.
* This ensures synchronized access as well as re-enabling echo after getting the Terminal.
*/
@deprecated(
"Don't use jline.Terminal directly. Use Terminal.get.withCanonicalIn instead.",
"1.4.0"
)
def usingTerminal[T](f: jline.Terminal => T): T = f(Terminal.get.toJLine)
@deprecated("unused", "1.4.0")
def createReader(): ConsoleReader = createReader(None, Terminal.wrappedSystemIn)
@deprecated("Use LineReader.createReader", "1.4.0")
def createReader(historyPath: Option[File], in: InputStream): ConsoleReader = {
val cr = Terminal.createReader(Terminal.console, Prompt.Running)
cr.setExpandEvents(false) // https://issues.scala-lang.org/browse/SI-7650
cr.setBellEnabled(false)
val h = historyPath match {
case None => new MemoryHistory
case Some(file) => new FileHistory(file): MemoryHistory
}
h.setMaxSize(MaxHistorySize)
cr.setHistory(h)
cr
}
@deprecated("Avoid referencing JLine directly.", "1.4.0")
def withJLine[T](action: => T): T = Terminal.get.withRawInput(action)
@deprecated("Use LineReader.simple instead", "1.4.0")
def simple(
historyPath: Option[File],
handleCONT: Boolean = LineReader.HandleCONT,
injectThreadSleep: Boolean = false
): SimpleReader = new SimpleReader(historyPath, handleCONT, injectThreadSleep)
@deprecated("Use LineReader.MaxHistorySize", "1.4.0")
val MaxHistorySize = LineReader.MaxHistorySize
@deprecated("Use LineReader.HandleCONT", "1.4.0")
val HandleCONT = LineReader.HandleCONT
}
@deprecated("For binary compatibility only", "1.4.0")
private[sbt] class InputStreamWrapper(is: InputStream, val poll: Duration)
extends FilterInputStream(is) {
@tailrec final override def read(): Int =
if (is.available() != 0) is.read()
else {
Thread.sleep(poll.toMillis)
read()
}
@tailrec final override def read(b: Array[Byte]): Int =
if (is.available() != 0) is.read(b)
else {
Thread.sleep(poll.toMillis)
read(b)
}
@tailrec final override def read(b: Array[Byte], off: Int, len: Int): Int =
if (is.available() != 0) is.read(b, off, len)
else {
Thread.sleep(poll.toMillis)
read(b, off, len)
}
}
final class FullReader(
historyPath: Option[File],
complete: Parser[_],
val handleCONT: Boolean,
terminal: Terminal
) extends JLine {
@deprecated("Use the constructor with no injectThreadSleep parameter", "1.4.0")
def this(
historyPath: Option[File],
complete: Parser[_],
handleCONT: Boolean = LineReader.HandleCONT,
injectThreadSleep: Boolean = false
) =
this(
historyPath,
complete,
handleCONT,
Terminal.console
)
protected[this] val reader: ConsoleReader = {
val cr = LineReader.createJLine2Reader(historyPath, terminal)
sbt.internal.util.complete.JLineCompletion.installCustomCompletor(cr, complete)
cr
}
}
class SimpleReader private[sbt] (
historyPath: Option[File],
val handleCONT: Boolean,
terminal: Terminal
) extends JLine {
def this(historyPath: Option[File], handleCONT: Boolean, injectThreadSleep: Boolean) =
this(historyPath, handleCONT, Terminal.console)
protected[this] lazy val reader: ConsoleReader =
LineReader.createJLine2Reader(historyPath, terminal)
}
object SimpleReader extends SimpleReader(None, LineReader.HandleCONT, false) {
def apply(terminal: Terminal): SimpleReader =
new SimpleReader(None, LineReader.HandleCONT, terminal)
}