aboutsummaryrefslogblamecommitdiff
path: root/compiler/src/dotty/tools/dotc/repl/ammonite/Terminal.scala
blob: 4b18b38e3a74d8fe11e3673cec4eb01c5e67b86b (plain) (tree)


















                                                                            




















































































                                                                                 
                                                             






















































































































































































































                                                                                                
package dotty.tools
package dotc
package repl
package ammonite
package terminal

import scala.annotation.tailrec
import scala.collection.mutable

/**
 * The core logic around a terminal; it defines the base `filters` API
 * through which anything (including basic cursor-navigation and typing)
 * interacts with the terminal.
 *
 * Maintains basic invariants, such as "cursor should always be within
 * the buffer", and "ansi terminal should reflect most up to date TermState"
 */
object Terminal {

  /**
   * Computes how tall a line of text is when wrapped at `width`.
   *
   * Even 0-character lines still take up one row!
   *
   * width = 2
   * 0 -> 1
   * 1 -> 1
   * 2 -> 1
   * 3 -> 2
   * 4 -> 2
   * 5 -> 3
   */
  def fragHeight(length: Int, width: Int) = math.max(1, (length - 1) / width + 1)

  def splitBuffer(buffer: Vector[Char]) = {
    val frags = mutable.Buffer.empty[Int]
    frags.append(0)
    for(c <- buffer){
      if (c == '\n') frags.append(0)
      else frags(frags.length - 1) = frags.last + 1
    }
    frags
  }
  def calculateHeight(buffer: Vector[Char],
                      width: Int,
                      prompt: String): Seq[Int] = {
    val rowLengths = splitBuffer(buffer)

    calculateHeight0(rowLengths, width - prompt.length)
  }

  /**
   * Given a buffer with characters and newlines, calculates how high
   * the buffer is and where the cursor goes inside of it.
   */
  def calculateHeight0(rowLengths: Seq[Int],
                       width: Int): Seq[Int] = {
    val fragHeights =
      rowLengths
        .inits
        .toVector
        .reverse // We want shortest-to-longest, inits gives longest-to-shortest
        .filter(_.nonEmpty) // Without the first empty prefix
        .map{ x =>
          fragHeight(
            // If the frag barely fits on one line, give it
            // an extra spot for the cursor on the next line
            x.last + 1,
            width
          )
        }
//    Debug("fragHeights " + fragHeights)
    fragHeights
  }

  def positionCursor(cursor: Int,
                     rowLengths: Seq[Int],
                     fragHeights: Seq[Int],
                     width: Int) = {
    var leftoverCursor = cursor
    //    Debug("leftoverCursor " + leftoverCursor)
    var totalPreHeight = 0
    var done = false
    // Don't check if the cursor exceeds the last chunk, because
    // even if it does there's nowhere else for it to go
    for(i <- 0 until rowLengths.length -1 if !done) {
      // length of frag and the '\n' after it
      val delta = rowLengths(i) + 1
      //      Debug("delta " + delta)
      val nextCursor = leftoverCursor - delta
      if (nextCursor >= 0) {
        //        Debug("nextCursor " + nextCursor)
        leftoverCursor = nextCursor
        totalPreHeight += fragHeights(i)
      }else done = true
    }

    val cursorY = totalPreHeight + leftoverCursor / width
    val cursorX = leftoverCursor % width

    (cursorY, cursorX)
  }


  type Action    = (Vector[Char], Int) => (Vector[Char], Int)
  type MsgAction = (Vector[Char], Int) => (Vector[Char], Int, String)


  def noTransform(x: Vector[Char], i: Int) = (Ansi.Str.parse(x), i)
  /**
   * Blockingly reads a line from the given input stream and returns it.
   *
   * @param prompt The prompt to display when requesting input
   * @param reader The input-stream where characters come in, e.g. System.in
   * @param writer The output-stream where print-outs go, e.g. System.out
   * @param filters A set of actions that can be taken depending on the input,
   * @param displayTransform code to manipulate the display of the buffer and
   *                         cursor, without actually changing the logical
   *                         values inside them.
   */
  def readLine(prompt: Prompt,
               reader: java.io.Reader,
               writer: java.io.Writer,
               filters: Filter,
               displayTransform: (Vector[Char], Int) => (Ansi.Str, Int) = noTransform)
               : Option[String] = {

    /**
      * Erases the previous line and re-draws it with the new buffer and
      * cursor.
      *
      * Relies on `ups` to know how "tall" the previous line was, to go up
      * and erase that many rows in the console. Performs a lot of horrific
      * math all over the place, incredibly prone to off-by-ones, in order
      * to at the end of the day position the cursor in the right spot.
      */
    def redrawLine(buffer: Ansi.Str,
                   cursor: Int,
                   ups: Int,
                   rowLengths: Seq[Int],
                   fullPrompt: Boolean = true,
                   newlinePrompt: Boolean = false) = {


      // Enable this in certain cases (e.g. cursor near the value you are
      // interested into) see what's going on with all the ansi screen-cursor
      // movement
      def debugDelay() = if (false){
        Thread.sleep(200)
        writer.flush()
      }


      val promptLine =
        if (fullPrompt) prompt.full
        else prompt.lastLine

      val promptWidth = if(newlinePrompt) 0 else prompt.lastLine.length
      val actualWidth = width - promptWidth

      ansi.up(ups)
      ansi.left(9999)
      ansi.clearScreen(0)
      writer.write(promptLine.toString)
      if (newlinePrompt) writer.write("\n")

      // I'm not sure why this is necessary, but it seems that without it, a
      // cursor that "barely" overshoots the end of a line, at the end of the
      // buffer, does not properly wrap and ends up dangling off the
      // right-edge of the terminal window!
      //
      // This causes problems later since the cursor is at the wrong X/Y,
      // confusing the rest of the math and ending up over-shooting on the
      // `ansi.up` calls, over-writing earlier lines. This prints a single
      // space such that instead of dangling it forces the cursor onto the
      // next line for-realz. If it isn't dangling the extra space is a no-op
      val lineStuffer = ' '
      // Under `newlinePrompt`, we print the thing almost-verbatim, since we
      // want to avoid breaking code by adding random indentation. If not, we
      // are guaranteed that the lines are short, so we can indent the newlines
      // without fear of wrapping
      val newlineReplacement =
        if (newlinePrompt) {

          Array(lineStuffer, '\n')
        } else {
          val indent = " " * prompt.lastLine.length
          Array('\n', indent:_*)
        }

      writer.write(
        buffer.render.flatMap{
          case '\n' => newlineReplacement
          case x => Array(x)
        }.toArray
      )
      writer.write(lineStuffer)

      val fragHeights = calculateHeight0(rowLengths, actualWidth)
      val (cursorY, cursorX) = positionCursor(
        cursor,
        rowLengths,
        fragHeights,
        actualWidth
      )
      ansi.up(fragHeights.sum - 1)
      ansi.left(9999)
      ansi.down(cursorY)
      ansi.right(cursorX)
      if (!newlinePrompt) ansi.right(prompt.lastLine.length)

      writer.flush()
    }

    @tailrec
    def readChar(lastState: TermState, ups: Int, fullPrompt: Boolean = true): Option[String] = {
      val moreInputComing = reader.ready()

      lazy val (transformedBuffer0, cursorOffset) = displayTransform(
        lastState.buffer,
        lastState.cursor
      )

      lazy val transformedBuffer = transformedBuffer0 ++ lastState.msg
      lazy val lastOffsetCursor = lastState.cursor + cursorOffset
      lazy val rowLengths = splitBuffer(
        lastState.buffer ++ lastState.msg.plainText
      )
      val narrowWidth = width - prompt.lastLine.length
      val newlinePrompt = rowLengths.exists(_ >= narrowWidth)
      val promptWidth = if(newlinePrompt) 0 else prompt.lastLine.length
      val actualWidth = width - promptWidth
      val newlineUp = if (newlinePrompt) 1 else 0
      if (!moreInputComing) redrawLine(
        transformedBuffer,
        lastOffsetCursor,
        ups,
        rowLengths,
        fullPrompt,
        newlinePrompt
      )

      lazy val (oldCursorY, _) = positionCursor(
        lastOffsetCursor,
        rowLengths,
        calculateHeight0(rowLengths, actualWidth),
        actualWidth
      )

      def updateState(s: LazyList[Int],
                      b: Vector[Char],
                      c: Int,
                      msg: Ansi.Str): (Int, TermState) = {

        val newCursor = math.max(math.min(c, b.length), 0)
        val nextUps =
          if (moreInputComing) ups
          else oldCursorY + newlineUp

        val newState = TermState(s, b, newCursor, msg)

        (nextUps, newState)
      }
      // `.get` because we assume that *some* filter is going to match each
      // character, even if only to dump the character to the screen. If nobody
      // matches the character then we can feel free to blow up
      filters.op(TermInfo(lastState, actualWidth)).get match {
        case Printing(TermState(s, b, c, msg), stdout) =>
          writer.write(stdout)
          val (nextUps, newState) = updateState(s, b, c, msg)
          readChar(newState, nextUps)

        case TermState(s, b, c, msg) =>
          val (nextUps, newState) = updateState(s, b, c, msg)
          readChar(newState, nextUps, false)

        case Result(s) =>
          redrawLine(
            transformedBuffer, lastState.buffer.length,
            oldCursorY + newlineUp, rowLengths, false, newlinePrompt
          )
          writer.write(10)
          writer.write(13)
          writer.flush()
          Some(s)
        case ClearScreen(ts) =>
          ansi.clearScreen(2)
          ansi.up(9999)
          ansi.left(9999)
          readChar(ts, ups)
        case Exit =>
          None
      }
    }

    lazy val ansi = new AnsiNav(writer)
    lazy val (width, _, initialConfig) = TTY.init()
    try {
      readChar(TermState(LazyList.continually(reader.read()), Vector.empty, 0, ""), 0)
    }finally{

      // Don't close these! Closing these closes stdin/stdout,
      // which seems to kill the entire program

      // reader.close()
      // writer.close()
      TTY.stty(initialConfig)
    }
  }
}
object Prompt {
  implicit def construct(prompt: String): Prompt = {
    val parsedPrompt = Ansi.Str.parse(prompt)
    val index = parsedPrompt.plainText.lastIndexOf('\n')
    val (_, last) = parsedPrompt.splitAt(index+1)
    Prompt(parsedPrompt, last)
  }
}

case class Prompt(full: Ansi.Str, lastLine: Ansi.Str)