/* sbt -- Simple Build Tool * Copyright 2011 Mark Harrah */ package sbt.complete import jline.{CandidateListCompletionHandler,Completor,CompletionHandler,ConsoleReader} import scala.annotation.tailrec import collection.JavaConversions object JLineCompletion { def installCustomCompletor(reader: ConsoleReader, parser: Parser[_]): Unit = installCustomCompletor(reader)(parserAsCompletor(parser)) def installCustomCompletor(reader: ConsoleReader)(complete: (String, Int) => (Seq[String], Seq[String])): Unit = installCustomCompletor(customCompletor(complete), reader) def installCustomCompletor(complete: (ConsoleReader, Int) => Boolean, reader: ConsoleReader): Unit = { reader.removeCompletor(DummyCompletor) reader.addCompletor(DummyCompletor) reader.setCompletionHandler(new CustomHandler(complete)) } private[this] final class CustomHandler(completeImpl: (ConsoleReader, Int) => Boolean) extends CompletionHandler { private[this] var previous: Option[(String,Int)] = None private[this] var level: Int = 1 override def complete(reader: ConsoleReader, candidates: java.util.List[_], position: Int) = { val current = Some(bufferSnapshot(reader)) level = if(current == previous) level + 1 else 1 previous = current try completeImpl(reader, level) catch { case e: Exception => reader.printString("\nException occurred while determining completions.") e.printStackTrace() false } } } // 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[_]): (String, Int) => (Seq[String], Seq[String]) = (str, level) => convertCompletions(Parser.completions(p, str, level)) def convertCompletions(c: Completions): (Seq[String], Seq[String]) = { val cs = c.get if(cs.isEmpty) (Nil, "{invalid input}" :: Nil) else convertCompletions(cs) } def convertCompletions(cs: Set[Completion]): (Seq[String], Seq[String]) = { val (insert, display) = ( (Set.empty[String], Set.empty[String]) /: cs) { case ( t @ (insert,display), comp) => if(comp.isEmpty) t else (insert + comp.append, appendNonEmpty(display, comp.display)) } (insert.toSeq, display.toSeq.sorted) } def appendNonEmpty(set: Set[String], add: String) = if(add.trim.isEmpty) set else set + add def customCompletor(f: (String, Int) => (Seq[String], Seq[String])): (ConsoleReader, Int) => Boolean = (reader, level) => { val success = complete(beforeCursor(reader), reader => f(reader, level), reader) reader.flushConsole() success } def bufferSnapshot(reader: ConsoleReader): (String, Int) = { val b = reader.getCursorBuffer (b.getBuffer.toString, b.cursor) } def beforeCursor(reader: ConsoleReader): String = { val b = reader.getCursorBuffer b.getBuffer.substring(0, b.cursor) } // returns false if there was nothing to insert and nothing to display def complete(beforeCursor: String, completions: String => (Seq[String],Seq[String]), reader: ConsoleReader): Boolean = { val (insert,display) = completions(beforeCursor) val common = commonPrefix(insert) if(common.isEmpty) if(display.isEmpty) () else showCompletions(display, reader) else appendCompletion(common, reader) !(common.isEmpty && display.isEmpty) } def appendCompletion(common: String, reader: ConsoleReader) { reader.getCursorBuffer.write(common) reader.redrawLine() } /** `display` is assumed to be the exact strings requested to be displayed. * In particular, duplicates should have been removed already. */ def showCompletions(display: Seq[String], reader: ConsoleReader) { printCompletions(display, reader) reader.drawLine() } def printCompletions(cs: Seq[String], reader: ConsoleReader) { val print = shouldPrint(cs, reader) reader.printNewline() if(print) printLinesAndColumns(cs, reader) } def printLinesAndColumns(cs: Seq[String], reader: ConsoleReader) { val (lines, columns) = cs partition hasNewline for(line <- lines) { reader.printString(line) if(line.charAt(line.length - 1) != '\n') reader.printNewline() } reader.printColumns(JavaConversions.asJavaList(columns.map(_.trim))) } def hasNewline(s: String): Boolean = s.indexOf('\n') >= 0 def shouldPrint(cs: Seq[String], reader: ConsoleReader): Boolean = { val size = cs.size (size <= reader.getAutoprintThreshhold) || confirm("Display all %d possibilities? (y or n) ".format(size), 'y', 'n', reader) } def confirm(prompt: String, trueC: Char, falseC: Char, reader: ConsoleReader): Boolean = { reader.printNewline() reader.printString(prompt) reader.flushConsole() reader.readCharacter( Array(trueC, falseC) ) == trueC } 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)) } }