From c83cd3313961528a9ee0abe618249a08eabba37f Mon Sep 17 00:00:00 2001 From: Erik Osheim Date: Mon, 10 Aug 2015 09:07:22 -0400 Subject: [PATCH 1/5] Add support for C-p/C-n for prev/next in history. This commit also makes some adjustments to the history code that hopefully make it a bit clearer what is going on. It also makes a change to cursor positioning. In most readline-based programs (e.g. bash), if the cursor is currently at the end of the line, it will remain at the end of the line after moving through the history. The current behavior leaves the cursor at position 0. With this change, a specific check is made to see if the cursor is at the end of line, and if so the cursor is moved to the "new" end of the line. --- .../ammonite/terminal/ReadlineFilters.scala | 28 +++++++++++++++---- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala b/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala index 87b0cbb5c..b0c80380e 100644 --- a/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala +++ b/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala @@ -85,23 +85,39 @@ object ReadlineFilters { def lastRow(cursor: Int, buffer: Vector[Char], width: Int) = { (buffer.length - cursor) < width && (buffer.lastIndexOf('\n') < cursor || buffer.lastIndexOf('\n') == -1) } + case class HistoryFilter(history: () => Seq[String]) extends TermCore.DelegateFilter{ var index = -1 var currentHistory = Vector[Char]() - def swapInHistory(b: Vector[Char], newIndex: Int, rest: LazyList[Int], c: Int) = { + def swapInHistory(b: Vector[Char], newIndex: Int, rest: LazyList[Int], c: Int): TermState = { if (index == -1 && newIndex != -1) currentHistory = b - + val h2 = if (newIndex == -1) currentHistory else history()(newIndex).toVector + val c2 = if (c == b.length) h2.length else c index = newIndex + TS(rest, h2, c2) + } - if (index == -1) TS(rest, currentHistory, c) - else TS(rest, history()(index).toVector, c) + def constrainIndex(n: Int): Int = { + val max = history().length - 1 + if (n < -1) -1 else if (max < n) max else n } + + def previousHistory(b: Vector[Char], rest: LazyList[Int], c: Int): TermState = + swapInHistory(b, constrainIndex(index + 1), rest, c) + + def nextHistory(b: Vector[Char], rest: LazyList[Int], c: Int): TermState = + swapInHistory(b, constrainIndex(index - 1), rest, c) + def filter = { case TermInfo(TS(p"\u001b[A$rest", b, c), w) if firstRow(c, b, w) => - swapInHistory(b, (index + 1) min (history().length - 1), rest, 99999) + previousHistory(b, rest, c) + case TermInfo(TS(p"\u0010$rest", b, c), w) if lastRow(c, b, w) => + previousHistory(b, rest, c) case TermInfo(TS(p"\u001b[B$rest", b, c), w) if lastRow(c, b, w) => - swapInHistory(b, (index - 1) max -1, rest, 0) + nextHistory(b, rest, c) + case TermInfo(TS(p"\u000e$rest", b, c), w) if firstRow(c, b, w) => + nextHistory(b, rest, c) } } } From bb9f061461dd62da46d4a6d98ecef5cb86c4f879 Mon Sep 17 00:00:00 2001 From: Erik Osheim Date: Tue, 11 Aug 2015 09:22:56 -0400 Subject: [PATCH 2/5] Simplify cursor logic in history navigation. Haoyi points out that things like bash always move the cursor to the end of the line during history navigation. So let's just do that here as well. --- .../scala/ammonite/terminal/ReadlineFilters.scala | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala b/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala index b0c80380e..bb36081d7 100644 --- a/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala +++ b/terminal/src/main/scala/ammonite/terminal/ReadlineFilters.scala @@ -90,12 +90,11 @@ object ReadlineFilters { var index = -1 var currentHistory = Vector[Char]() - def swapInHistory(b: Vector[Char], newIndex: Int, rest: LazyList[Int], c: Int): TermState = { + def swapInHistory(b: Vector[Char], newIndex: Int, rest: LazyList[Int]): TermState = { if (index == -1 && newIndex != -1) currentHistory = b - val h2 = if (newIndex == -1) currentHistory else history()(newIndex).toVector - val c2 = if (c == b.length) h2.length else c index = newIndex - TS(rest, h2, c2) + val h = if (index == -1) currentHistory else history()(index).toVector + TS(rest, h, h.length) } def constrainIndex(n: Int): Int = { @@ -104,10 +103,10 @@ object ReadlineFilters { } def previousHistory(b: Vector[Char], rest: LazyList[Int], c: Int): TermState = - swapInHistory(b, constrainIndex(index + 1), rest, c) + swapInHistory(b, constrainIndex(index + 1), rest) def nextHistory(b: Vector[Char], rest: LazyList[Int], c: Int): TermState = - swapInHistory(b, constrainIndex(index - 1), rest, c) + swapInHistory(b, constrainIndex(index - 1), rest) def filter = { case TermInfo(TS(p"\u001b[A$rest", b, c), w) if firstRow(c, b, w) => From fba071f8247127b2866baf872bde707712b62a46 Mon Sep 17 00:00:00 2001 From: Erik Osheim Date: Tue, 11 Aug 2015 09:32:34 -0400 Subject: [PATCH 3/5] Improve the REPL's startup banner. This commit does a few things: * Shorten the "loading" message * Include Ammonite version in banner * Include Scala and Java versions in banner too --- repl/src/main/scala/ammonite/repl/Repl.scala | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/repl/src/main/scala/ammonite/repl/Repl.scala b/repl/src/main/scala/ammonite/repl/Repl.scala index 7fbd0c1a6..53d0b1c5c 100644 --- a/repl/src/main/scala/ammonite/repl/Repl.scala +++ b/repl/src/main/scala/ammonite/repl/Repl.scala @@ -1,6 +1,7 @@ package ammonite.repl import java.io._ +import ammonite.Constants import ammonite.repl.Util.IvyMap import ammonite.repl.frontend._ import acyclic.file @@ -62,8 +63,22 @@ class Repl(input: InputStream, out } + def ammoniteVersion: String = + Constants.version + + def scalaVersion: String = + scala.util.Properties.versionString + + def javaVersion: String = + System.getProperty("java.version") + + def printBanner(): Unit = { + printer.println(s"Welcome to the Ammonite Repl $ammoniteVersion") + printer.println(s"(Scala $scalaVersion Java $javaVersion)") + } def run() = { + printBanner() @tailrec def loop(): Unit = { val res = action() res match{ @@ -114,7 +129,7 @@ object Repl{ def defaultAmmoniteHome = Path(System.getProperty("user.home"))/".ammonite" def main(args: Array[String]) = { val parser = new scopt.OptionParser[Config]("ammonite") { - head("ammonite", ammonite.Constants.version) + head("ammonite", Constants.version) opt[String]('p', "predef") .action((x, c) => c.copy(predef = x)) .text("Any commands you want to execute at the start of the REPL session") @@ -142,7 +157,7 @@ object Repl{ ) file match{ case None => - println("Loading Ammonite Repl...") + println("Loading...") repl.run() case Some(path) => repl.interp.replApi.load.module(path) From df72f00d898b04a68dc9caf6c6b6c8ad4f7599b6 Mon Sep 17 00:00:00 2001 From: Erik Osheim Date: Tue, 11 Aug 2015 09:39:05 -0400 Subject: [PATCH 4/5] Enable C-d to delete when line is not empty. Most tools only use C-d to exit when the line is empty. Previously a C-d would exit instead of delete, which will be surprising for bash users. --- .../main/scala/ammonite/terminal/BasicFilters.scala | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala b/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala index 03503604d..5cfb03608 100644 --- a/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala +++ b/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala @@ -73,9 +73,17 @@ object BasicFilters { case TS(13 ~: 10 ~:rest, b, c) => // Enter Result(b.mkString) } + lazy val exitFilter: TermCore.Filter = { - case TS(Ctrl('c') ~: rest, b, c) => Result("") - case TS(Ctrl('d') ~: rest, b, c) => Exit + case TS(Ctrl('c') ~: rest, b, c) => + Result("") + case TS(Ctrl('d') ~: rest, b, c) => + // only exit if the line is empty, otherwise, behave like + // "delete" (i.e. delete one char to the right) + if (b.isEmpty) Exit else { + val (first, last) = b.splitAt(c) + TS(rest, first ++ last.drop(1), c) + } } def moveStart(b: Vector[Char], From f3ddaa7acf53dc2445454b777cf70d0e649f33ca Mon Sep 17 00:00:00 2001 From: Erik Osheim Date: Tue, 11 Aug 2015 10:03:51 -0400 Subject: [PATCH 5/5] Support clearing the screen via C-l. This should work the same as bash et al. If there is an "in progress" line it is preserved and redrawn at position (0, 0). --- terminal/src/main/scala/ammonite/terminal/BasicFilters.scala | 4 ++++ terminal/src/main/scala/ammonite/terminal/TermCore.scala | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala b/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala index 03503604d..34ea369f9 100644 --- a/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala +++ b/terminal/src/main/scala/ammonite/terminal/BasicFilters.scala @@ -13,6 +13,7 @@ object BasicFilters { navFilter orElse exitFilter orElse enterFilter orElse + clearFilter orElse loggingFilter orElse typingFilter } @@ -77,6 +78,9 @@ object BasicFilters { case TS(Ctrl('c') ~: rest, b, c) => Result("") case TS(Ctrl('d') ~: rest, b, c) => Exit } + lazy val clearFilter: TermCore.Filter = { + case TS(Ctrl('l') ~: rest, b, c) => ClearScreen(TS(rest, b, c)) + } def moveStart(b: Vector[Char], c: Int, diff --git a/terminal/src/main/scala/ammonite/terminal/TermCore.scala b/terminal/src/main/scala/ammonite/terminal/TermCore.scala index 3e66a5cd8..cf54e01ae 100644 --- a/terminal/src/main/scala/ammonite/terminal/TermCore.scala +++ b/terminal/src/main/scala/ammonite/terminal/TermCore.scala @@ -197,6 +197,10 @@ object TermCore { writer.write(13) writer.flush() Some(s) + case ClearScreen(ts) => + ansi.clearScreen(2) + ansi.moveTo(0, 0) + readChar(ts, ups) case Exit => None } @@ -234,5 +238,6 @@ object TermState{ } } +case class ClearScreen(ts: TermState) extends TermAction case object Exit extends TermAction case class Result(s: String) extends TermAction