diff options
Diffstat (limited to 'src/dotty/tools/dotc/repl/ammonite/Terminal.scala')
-rw-r--r-- | src/dotty/tools/dotc/repl/ammonite/Terminal.scala | 320 |
1 files changed, 0 insertions, 320 deletions
diff --git a/src/dotty/tools/dotc/repl/ammonite/Terminal.scala b/src/dotty/tools/dotc/repl/ammonite/Terminal.scala deleted file mode 100644 index 4b18b38e3..000000000 --- a/src/dotty/tools/dotc/repl/ammonite/Terminal.scala +++ /dev/null @@ -1,320 +0,0 @@ -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) |