All Downloads are FREE. Search and download functionalities are using the official Maven repository.

ammonite.repl.FrontEnd.scala Maven / Gradle / Ivy

The newest version!
package ammonite.repl

import java.io.{InputStream, OutputStream}
import scala.collection.JavaConverters._
import fastparse.core.Parsed
import fastparse.utils.ParserInput
import org.jline.reader._
import org.jline.reader.impl.history.DefaultHistory
import org.jline.reader.impl.DefaultParser.ArgumentList
import org.jline.terminal._
import org.jline.utils.AttributedString
import ammonite.util.{Catching, Colors, Res}
import ammonite.interp.Parsers

/**
 * All the mucky JLine interfacing code
 */
trait FrontEnd{
  def width: Int
  def height: Int
  def action(input: InputStream,
             reader: java.io.Reader,
             output: OutputStream,
             prompt: String,
             colors: Colors,
             compilerComplete: (Int, String) => (Int, Seq[String], Seq[String]),
             history: IndexedSeq[String],
             addHistory: String => Unit): Res[(String, Seq[String])]
}

object FrontEnd{
  object JLineUnix extends JLineTerm
  object JLineWindows extends JLineTerm
  class JLineTerm() extends FrontEnd{

    private val term = TerminalBuilder.builder().build()
    private val readerBuilder = LineReaderBuilder.builder().terminal(term)
    private val ammHighlighter = new AmmHighlighter()
    private val ammCompleter = new AmmCompleter(ammHighlighter)
    private val ammParser = new AmmParser()
    readerBuilder.highlighter(ammHighlighter)
    readerBuilder.completer(ammCompleter)
    readerBuilder.parser(ammParser)
    readerBuilder.history(new DefaultHistory())
    readerBuilder.option(LineReader.Option.DISABLE_EVENT_EXPANSION, true)
    readerBuilder.option(LineReader.Option.INSERT_TAB, true)
    private val reader = readerBuilder.build()

    def width = term.getWidth
    def height = term.getHeight

    def action(jInput: InputStream,
               jReader: java.io.Reader,
               jOutput: OutputStream,
               prompt: String,
               colors: Colors,
               compilerComplete: (Int, String) => (Int, Seq[String], Seq[String]),
               historyValues: IndexedSeq[String],
               addHistory: String => Unit) = {

      ammCompleter.compilerComplete = compilerComplete
      ammParser.addHistory = addHistory
      ammHighlighter.colors = colors
      historyValues.foreach(reader.getHistory.add)

      def readCode(): Res[(String, Seq[String])] = {
        Option(reader.readLine(prompt)) match {
          case Some(code) =>
            val pl = reader.getParser.parse(code, 0)
            Res.Success(code -> pl.words().asScala)
          case None => Res.Exit(())
        }
      }

      for {
        _ <- Catching {
          case e: UserInterruptException =>
            if (e.getPartialLine == "") {
              term.writer().println("Ctrl-D to exit")
              term.flush()
            }
            Res.Skip
          case e: SyntaxError =>
            Res.Failure(e.msg)
          case e: EndOfFileException =>
            Res.Exit("user exited")
        }
        res <- readCode()
      } yield res
    }
  }
}

class AmmCompleter(highlighter: Highlighter) extends Completer {
  // completion varies from action to action
  var compilerComplete: (Int, String) => (Int, Seq[String], Seq[String]) =
    (x, y) => (0, Seq.empty, Seq.empty)

  override def complete(reader: LineReader,
                        line: ParsedLine,
                        candidates: java.util.List[Candidate]): Unit = {
    val (completionBase, completions, sigs) = compilerComplete(
      line.cursor(),
      line.line()
    )
    // display method signature(s)
    if (sigs.nonEmpty) {
      reader.getTerminal.writer.println()
      sigs.foreach{ sig =>
        val sigHighlighted = highlighter.highlight(reader, sig).toAnsi
        reader.getTerminal.writer.println(sigHighlighted)
      }
      reader.callWidget(LineReader.REDRAW_LINE)
      reader.callWidget(LineReader.REDISPLAY)
      reader.getTerminal.flush()
    }
    // add suggestions
    completions.sorted.foreach { c =>
      // if member selection, concatenate compiler suggestion to variable name
      val candidate = if (line.word().contains(".")) {
        val lastDotIndex = line.word().lastIndexOf(".")
        val prefix = line.word().substring(0, lastDotIndex + 1)
        prefix + c
      } else {
        c
      }
      candidates.add(new Candidate(candidate, c, null, null, null, null, false))
    }
  }
}

class AmmParser extends Parser {

  var addHistory: String => Unit = x => ()

  override def parse(line: String, cursor: Int, context: Parser.ParseContext): ParsedLine = {
    val words = new java.util.ArrayList[String]()
    var wordCursor = -1
    var wordIndex = -1
    Parsers.split(line) match {
      case Some(Parsed.Success(value, idx)) =>
        addHistory(line)
        words.addAll(value.asJava)
        if (cursor == line.length && words.size > 0) {
          wordIndex = words.size - 1
          wordCursor = words.get(words.size - 1).length
        }
        new ArgumentList(line, words, wordIndex, wordCursor, cursor)
      case Some(Parsed.Failure(p, idx, extra)) =>
        // we "accept the failure" only when ENTER is pressed, loops forever otherwise...
        // https://groups.google.com/d/msg/jline-users/84fPur0oHKQ/bRnjOJM4BAAJ
        if (context == Parser.ParseContext.ACCEPT_LINE) {
          addHistory(line)
          throw new SyntaxError(
            fastparse.core.ParseError.msg(extra.input, extra.traced.expected, idx)
          )
        } else {
          new ArgumentList(line, words, wordIndex, wordCursor, cursor)
        }
      case None =>
        // when TAB is pressed (COMEPLETE context) return a line so that it can show suggestions
        // else throw EOFError to signal that input isn't finished
        if (context == Parser.ParseContext.COMPLETE) {
          new ArgumentList(line, words, wordIndex, wordCursor, cursor)
        } else {
          throw new EOFError(-1, -1, "Missing closing paren/quote/expression")
        }
    }
  }
}

class SyntaxError(val msg: String) extends RuntimeException

class AmmHighlighter extends Highlighter {

  var colors: Colors = Colors.Default

  override def highlight(reader: LineReader, buffer: String): AttributedString = {
    val hl = Highlighter.defaultHighlight(
      buffer.toVector,
      colors.comment(),
      colors.`type`(),
      colors.literal(),
      colors.keyword(),
      fansi.Attr.Reset
    ).mkString
    AttributedString.fromAnsi(hl)
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy