diff options
-rwxr-xr-x | build.xml | 1 | ||||
-rw-r--r-- | src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala | 123 | ||||
-rw-r--r-- | src/repl/scala/tools/nsc/interpreter/JLineReader.scala | 6 | ||||
-rw-r--r-- | src/repl/scala/tools/nsc/interpreter/ReplConfig.scala | 4 | ||||
-rw-r--r-- | src/repl/scala/tools/nsc/interpreter/ReplProps.scala | 7 | ||||
-rw-r--r-- | test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala | 85 |
6 files changed, 212 insertions, 14 deletions
@@ -977,6 +977,7 @@ TODO: <path id="test.junit.compiler.build.path"> <pathelement location="${test.junit.classes}"/> <path refid="quick.compiler.build.path"/> + <path refid="quick.repl.build.path"/> <path refid="junit.classpath"/> </path> diff --git a/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala b/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala index cf03ecb480..d8efcda8b5 100644 --- a/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala +++ b/src/repl/scala/tools/nsc/interpreter/ConsoleReaderHelper.scala @@ -8,7 +8,9 @@ package interpreter import jline.console.{ ConsoleReader, CursorBuffer } -trait ConsoleReaderHelper extends ConsoleReader { +trait ConsoleReaderHelper { _: ConsoleReader with Tabulator => + def isAcross: Boolean + def terminal = getTerminal() def width = terminal.getWidth() def height = terminal.getHeight() @@ -16,7 +18,8 @@ trait ConsoleReaderHelper extends ConsoleReader { def readOneKey(prompt: String): Int def eraseLine(): Unit - private val marginSize = 3 + val marginSize = 3 + private def morePrompt = "--More--" private def emulateMore(): Int = { val key = readOneKey(morePrompt) @@ -38,19 +41,12 @@ trait ConsoleReaderHelper extends ConsoleReader { } override def printColumns(items: JCollection[_ <: CharSequence]): Unit = - printColumns(items: List[String]) - - def printColumns(items: List[String]): Unit = { - if (items forall (_ == "")) - return + printColumns_(items: List[String]) - val longest = items map (_.length) max + private def printColumns_(items: List[String]): Unit = if (items exists (_ != "")) { + val grouped = tabulate(items) var linesLeft = if (isPaginationEnabled()) height - 1 else Int.MaxValue - val columnSize = longest + marginSize - val padded = items map ("%-" + columnSize + "s" format _) - val groupSize = 1 max (width / columnSize) // make sure it doesn't divide to 0 - - padded grouped groupSize foreach { xs => + grouped foreach { xs => println(xs.mkString) linesLeft -= 1 if (linesLeft <= 0) { @@ -61,3 +57,104 @@ trait ConsoleReaderHelper extends ConsoleReader { } } } + +trait Tabulator { + def isAcross: Boolean + def width: Int + def marginSize: Int + + protected def fits(items: Seq[String], width: Int): Boolean = ( + (items map (_.length)).sum + (items.length - 1) * marginSize < width + ) + def tabulate(items: Seq[String]): Seq[Seq[String]] = ( + if (fits(items, width)) Seq(Seq(items mkString " " * marginSize)) + else printMultiLineColumns(items) + ) + protected def columnize(ss: Seq[String]): Seq[Seq[String]] = ss map (s => Seq(s)) + protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = { + import SimpleMath._ + val longest = (items map (_.length)).max + val columnWidth = longest + marginSize + val maxcols = ( + if (columnWidth >= width) 1 + else 1 max (width / columnWidth) // make sure it doesn't divide to 0 + ) + val nrows = items.size /% maxcols + val ncols = items.size /% nrows + val groupSize = ncols + val padded = items map (s"%-${columnWidth}s" format _) + val xwise = isAcross || ncols >= items.length + val grouped: Seq[Seq[String]] = + if (groupSize == 1) columnize(items) + else if (xwise) (padded grouped groupSize).toSeq + else { + val h = 1 max padded.size /% groupSize + val cols = (padded grouped h).toList + for (i <- 0 until h) yield + for (j <- 0 until groupSize) yield + if (i < cols(j).size) cols(j)(i) else "" + } + grouped + } +} + +/** Adjust the column width and number of columns to minimize the row count. */ +trait VariColumnTabulator extends Tabulator { + override protected def printMultiLineColumns(items: Seq[String]): Seq[Seq[String]] = { + import SimpleMath._ + val longest = (items map (_.length)).max + val shortest = (items map (_.length)).min + val fattest = longest + marginSize + val skinny = shortest + marginSize + + // given ncols, calculate nrows and a list of column widths, or none if not possible + // if ncols > items.size, then columnWidths.size == items.size + def layout(ncols: Int): Option[(Int, Seq[Int], Seq[Seq[String]])] = { + val nrows = items.size /% ncols + val xwise = isAcross || ncols >= items.length + def maxima(sss: Seq[Seq[String]]) = + (0 until (ncols min items.size)) map (i => (sss map (ss => ss(i).length)).max) + def resulting(rows: Seq[Seq[String]]) = { + val columnWidths = maxima(rows) map (_ + marginSize) + val linelen = columnWidths.sum + if (linelen <= width) Some((nrows, columnWidths, rows)) + else None + } + if (ncols == 1) resulting(columnize(items)) + else if (xwise) resulting((items grouped ncols).toSeq) + else { + val cols = (items grouped nrows).toList + val rows = for (i <- 0 until nrows) yield + for (j <- 0 until ncols) yield + if (j < cols.size && i < cols(j).size) cols(j)(i) else "" + resulting(rows) + } + } + + if (fattest >= width) { + columnize(items) + } else { + // if every col is widest, we have at least this many cols + val mincols = 1 max (width / fattest) + // if every other col is skinniest, we have at most this many cols + val maxcols = 1 + ((width - fattest) / skinny) + val possibles = (mincols to maxcols).map(n => layout(n)).flatten + val minrows = (possibles map (_._1)).min + + // select the min ncols that results in minrows + val (_, columnWidths, sss) = (possibles find (_._1 == minrows)).get + + // format to column width + sss map (ss => ss.zipWithIndex map { + case (s, i) => s"%-${columnWidths(i)}s" format s + }) + } + } +} + +private[interpreter] object SimpleMath { + implicit class DivRem(private val i: Int) extends AnyVal { + /** i/n + if (i % n != 0) 1 else 0 */ + def /%(n: Int): Int = (i + n - 1) / n + } +} diff --git a/src/repl/scala/tools/nsc/interpreter/JLineReader.scala b/src/repl/scala/tools/nsc/interpreter/JLineReader.scala index 8b0c6d78fa..b6e834a1ed 100644 --- a/src/repl/scala/tools/nsc/interpreter/JLineReader.scala +++ b/src/repl/scala/tools/nsc/interpreter/JLineReader.scala @@ -33,7 +33,11 @@ class JLineReader(_completion: => Completion) extends InteractiveReader { } } - class JLineConsoleReader extends ConsoleReader with ConsoleReaderHelper { + class JLineConsoleReader extends ConsoleReader with ConsoleReaderHelper with VariColumnTabulator { + val isAcross = interpreter.`package`.isAcross + + this setPaginationEnabled interpreter.`package`.isPaged + // ASAP this setExpandEvents false diff --git a/src/repl/scala/tools/nsc/interpreter/ReplConfig.scala b/src/repl/scala/tools/nsc/interpreter/ReplConfig.scala index 3392ea0b5e..046d6ecbfb 100644 --- a/src/repl/scala/tools/nsc/interpreter/ReplConfig.scala +++ b/src/repl/scala/tools/nsc/interpreter/ReplConfig.scala @@ -46,4 +46,8 @@ trait ReplConfig { def isReplDebug: Boolean = replProps.debug || isReplTrace def isReplInfo: Boolean = replProps.info || isReplDebug def isReplPower: Boolean = replProps.power + + private def csv(p: String, v: String) = p split "," contains v + def isPaged: Boolean = replProps.format.isSet && csv(replProps.format.get, "paged") + def isAcross: Boolean = replProps.format.isSet && csv(replProps.format.get, "across") } diff --git a/src/repl/scala/tools/nsc/interpreter/ReplProps.scala b/src/repl/scala/tools/nsc/interpreter/ReplProps.scala index 2364918494..36e6dbbccc 100644 --- a/src/repl/scala/tools/nsc/interpreter/ReplProps.scala +++ b/src/repl/scala/tools/nsc/interpreter/ReplProps.scala @@ -18,6 +18,13 @@ class ReplProps { val trace = bool("scala.repl.trace") val power = bool("scala.repl.power") + /** CSV of paged,across to enable pagination or `-x` style + * columns, "across" instead of down the column. Since + * pagination turns off columnar output, these flags are + * currently mutually exclusive. + */ + val format = Prop[String]("scala.repl.format") + val replAutorunCode = Prop[JFile]("scala.repl.autoruncode") val powerInitCode = Prop[JFile]("scala.repl.power.initcode") val powerBanner = Prop[JFile]("scala.repl.power.banner") diff --git a/test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala b/test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala new file mode 100644 index 0000000000..21e338eac0 --- /dev/null +++ b/test/junit/scala/tools/nsc/interpreter/TabulatorTest.scala @@ -0,0 +1,85 @@ +package scala.tools.nsc +package interpreter + +//import org.junit.Assert._ +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +case class Tabby(width: Int = 80, isAcross: Boolean = false, marginSize: Int = 3) extends Tabulator +case class VTabby(width: Int = 80, isAcross: Boolean = false, marginSize: Int = 3) extends VariColumnTabulator + +@RunWith(classOf[JUnit4]) +class TabulatorTest { + + @Test def oneliner() = { + val sut = Tabby() + val items = List("a", "b", "c") + val res = sut tabulate items + assert(res.size == 1) + assert(res(0).size == 1) + assert(res(0)(0) startsWith "a") + assert(res(0)(0) endsWith "c") + } + @Test def twoliner() = { + val sut = Tabby(width = 40) + val items = List("a" * 15, "b" * 15, "c" * 15) + val res = sut tabulate items + assert(res.size == 2) + assert(res(0).size == 2) + assert(res(1).size == 2) // trailing empty strings + assert(res(1)(0) startsWith "b") + } + @Test def twolinerx() = { + val sut = Tabby(width = 40, isAcross = true) + val items = List("a" * 15, "b" * 15, "c" * 15) + val res = sut tabulate items + assert(res.size == 2) + assert(res(0).size == 2) + assert(res(1).size == 1) // no trailing empty strings + assert(res(1)(0) startsWith "c") + } + // before, two 9-width cols don't fit in 20 + // but now, 5-col and 9-col do fit. + @Test def twolinerVariable() = { + val sut = VTabby(width = 20) + val items = (1 to 9) map (i => i.toString * i) + val rows = sut tabulate items + assert(rows.size == 5) + assert(rows(0).size == 2) + assert(rows(0)(0).size == 8) // width is 55555 plus margin of 3 + } + @Test def sys() = { + val sut = VTabby(width = 40) + val items = List("BooleanProp", "PropImpl", "addShutdownHook", "error", + "process", "CreatorImpl", "ShutdownHookThread", "allThreads", + "exit", "props", "Prop", "SystemProperties", + "env", "package", "runtime") + val rows = sut tabulate items + assert(rows.size == 8) + assert(rows(0).size == 2) + assert(rows(0)(0).size == "ShutdownHookThread".length + sut.marginSize) // 21 + } + @Test def syswide() = { + val sut = VTabby(width = 120) + val items = List("BooleanProp", "PropImpl", "addShutdownHook", "error", + "process", "CreatorImpl", "ShutdownHookThread", "allThreads", + "exit", "props", "Prop", "SystemProperties", + "env", "package", "runtime") + val rows = sut tabulate items + assert(rows.size == 2) + assert(rows(0).size == 8) + assert(rows(0)(0).size == "BooleanProp".length + sut.marginSize) // 14 + } + @Test def resultFits() = { + val sut = VTabby(width = 10) + // each of two lines would fit, but layout is two cols of width six > 10 + // therefore, should choose ncols = 1 + val items = List("a", "bcd", + "efg", "h") + val rows = sut tabulate items + assert(rows.size == 4) + assert(rows(0).size == 1) + assert(rows(0)(0).size == "efg".length + sut.marginSize) // 6 + } +} |