From 8af2a0b7e0cccce5648794c7a27786a19984583e Mon Sep 17 00:00:00 2001 From: Mark Harrah Date: Tue, 18 Jan 2011 18:07:48 -0500 Subject: [PATCH] JLine integration for tab completion combinators --- util/complete/JLineCompletion.scala | 94 +++++++++++++++++++ util/complete/src/test/scala/ParserTest.scala | 20 ++++ 2 files changed, 114 insertions(+) create mode 100644 util/complete/JLineCompletion.scala diff --git a/util/complete/JLineCompletion.scala b/util/complete/JLineCompletion.scala new file mode 100644 index 000000000..f9c7183ca --- /dev/null +++ b/util/complete/JLineCompletion.scala @@ -0,0 +1,94 @@ +/* sbt -- Simple Build Tool + * Copyright 2011 Mark Harrah + */ +package sbt.parse + + import jline.{CandidateListCompletionHandler,Completor,CompletionHandler,ConsoleReader} + import scala.annotation.tailrec + import collection.JavaConversions + +object JLineCompletion +{ + def installCustomCompletor(reader: ConsoleReader, parser: Parser[_]): Unit = + installCustomCompletor(parserAsCompletor(parser), reader) + def installCustomCompletor(reader: ConsoleReader)(complete: String => (Seq[String], Seq[String])): Unit = + installCustomCompletor(customCompletor(complete), reader) + def installCustomCompletor(complete: ConsoleReader => Boolean, reader: ConsoleReader): Unit = + { + reader.removeCompletor(DummyCompletor) + reader.addCompletor(DummyCompletor) + reader.setCompletionHandler(new CustomHandler(complete)) + } + + private[this] final class CustomHandler(completeImpl: ConsoleReader => Boolean) extends CompletionHandler + { + override def complete(reader: ConsoleReader, candidates: java.util.List[_], position: Int) = completeImpl(reader) + } + + // always provides dummy completions so that the custom completion handler gets called + // (ConsoleReader doesn't call the handler if there aren't any completions) + // the custom handler will then throw away the candidates and call the custom function + private[this] final object DummyCompletor extends Completor + { + override def complete(buffer: String, cursor: Int, candidates: java.util.List[_]): Int = + { + candidates.asInstanceOf[java.util.List[String]] add "dummy" + 0 + } + } + + def parserAsCompletor(p: Parser[_]): ConsoleReader => Boolean = + customCompletor(str => convertCompletions(Parser.completions(p, str))) + def convertCompletions(c: Completions): (Seq[String], Seq[String]) = + { + ( (Seq[String](), Seq[String]()) /: c.get) { case ( t @ (insert,display), comp) => + if(comp.isEmpty) t else (insert :+ comp.append, insert :+ comp.display) + } + } + + def customCompletor(f: String => (Seq[String], Seq[String])): ConsoleReader => Boolean = + reader => { + val success = complete(beforeCursor(reader), f, reader, false) + reader.flushConsole() + success + } + + def beforeCursor(reader: ConsoleReader): String = + { + val b = reader.getCursorBuffer + b.getBuffer.substring(0, b.cursor) + } + + def complete(beforeCursor: String, completions: String => (Seq[String],Seq[String]), reader: ConsoleReader, inserted: Boolean): Boolean = + { + val (insert,display) = completions(beforeCursor) + if(insert.isEmpty) + inserted + else + { + lazy val common = commonPrefix(insert) + if(inserted || common.isEmpty) + { + showCompletions(display, reader) + reader.drawLine() + true + } + else + { + reader.getCursorBuffer.write(common) + reader.redrawLine() + complete(beforeCursor + common, completions, reader, true) + } + } + } + def showCompletions(cs: Seq[String], reader: ConsoleReader): Unit = + if(cs.isEmpty) () else CandidateListCompletionHandler.printCandidates(reader, JavaConversions.asJavaList(cs), true) + + def commonPrefix(s: Seq[String]): String = if(s.isEmpty) "" else s reduceLeft commonPrefix + def commonPrefix(a: String, b: String): String = + { + val len = a.length min b.length + @tailrec def loop(i: Int): Int = if(i >= len) len else if(a(i) != b(i)) i else loop(i+1) + a.substring(0, loop(0)) + } +} \ No newline at end of file diff --git a/util/complete/src/test/scala/ParserTest.scala b/util/complete/src/test/scala/ParserTest.scala index 3a2bc494f..2cb907bc1 100644 --- a/util/complete/src/test/scala/ParserTest.scala +++ b/util/complete/src/test/scala/ParserTest.scala @@ -3,6 +3,26 @@ package sbt.parse import Parser._ import org.scalacheck._ +object JLineTest +{ + def main(args: Array[String]) + { + import jline.{ConsoleReader,Terminal} + val reader = new ConsoleReader() + Terminal.getTerminal.disableEcho() + + val parser = ParserExample.t + JLineCompletion.installCustomCompletor(reader, parser) + def loop() { + val line = reader.readLine("> ") + if(line ne null) { + println("Entered '" + line + "'") + loop() + } + } + loop() + } +} object ParserTest extends Properties("Completing Parser") { val wsc = charClass(_.isWhitespace)