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

ammonite.compiler.Pressy.scala Maven / Gradle / Ivy

The newest version!
package ammonite.compiler

import java.io.{OutputStream, PrintWriter}

import ammonite.compiler.MakeReporter.makeReporter
import ammonite.util.Name

import scala.reflect.internal.util.{BatchSourceFile, OffsetPosition, Position}
import scala.reflect.io.VirtualDirectory
import scala.tools.nsc
import scala.tools.nsc.Settings
import scala.tools.nsc.interactive.Response
import scala.util.{Failure, Success, Try}
import ammonite.util.Util.newLine
import scala.tools.nsc.interactive.{Global => InteractiveGlobal}
import scala.tools.nsc.classpath.AggregateClassPath
import scala.tools.nsc.reporters.Reporter

/**
 * Nice wrapper for the presentation compiler.
 */
trait Pressy {

  /**
   * Ask for autocompletion at a particular spot in the code, returning
   * possible things that can be completed at that location. May try various
   * different completions depending on where the `index` is placed, but
   * the outside caller probably doesn't care.
   *
   * The returned index gives the position from which the possible replacements
   * should be inserted.
   */
  def complete(
      snippetIndex: Int,
      previousImports: String,
      snippet: String
  ): (Int, Seq[String], Seq[String])
  def compiler: nsc.interactive.Global
  def shutdownPressy(): Unit
}
object Pressy {

  /**
   * Encapsulates all the logic around a single instance of
   * `nsc.interactive.Global` and other data specific to a single completion
   */
  class Run(
      val pressy: nsc.interactive.Global,
      currentFile: BatchSourceFile,
      dependencyCompleteOpt: Option[String => (Int, Seq[String])],
      allCode: String,
      index: Int
  ) {

    val blacklistedPackages = Set("shaded")

    /**
     * Dumb things that turn up in the autocomplete that nobody needs or wants
     */
    def blacklisted(s: pressy.Symbol) = {
      val blacklist = Set(
        "scala.Predef.any2stringadd.+",
        "scala.Any.##",
        "java.lang.Object.##",
        "scala.",
        "scala.",
        "scala.",
        "scala.",
        "scala.Predef.StringFormat.formatted",
        "scala.Predef.Ensuring.ensuring",
        "scala.Predef.ArrowAssoc.->",
        "scala.Predef.ArrowAssoc.→",
        "java.lang.Object.synchronized",
        "java.lang.Object.ne",
        "java.lang.Object.eq",
        "java.lang.Object.wait",
        "java.lang.Object.notifyAll",
        "java.lang.Object.notify"
      )

      blacklist(s.fullNameAsName('.').decoded) ||
      s.isImplicit ||
      // Cache objects, which you should probably never need to
      // access directly, and apart from that have annoyingly long names
      "cache[a-f0-9]{32}".r.findPrefixMatchOf(s.name.decoded).isDefined ||
      s.isDeprecated ||
      s.decodedName == "" ||
      s.decodedName.contains('$')
    }

    private val memberToString = {
      // Some hackery here to get at the protected CodePrinter.printedName which was the only
      // mildly reusable prior art I could locate. Other related bits:
      // - When constructing Trees, Scala captures the back-quoted nature into the AST as
      //   Ident.isBackquoted. Code-completion is inherently "pre-Tree", at least for the
      //   symbols being considered for use as completions, so not clear how this would be
      //   leveragable without big changes inside nsc.
      // - There's a public-but-incomplete implementation of rule-based backquoting in
      //   Printers.quotedName
      val nullOutputStream = new OutputStream() { def write(b: Int): Unit = {} }
      val backQuoter = new pressy.CodePrinter(
        new PrintWriter(nullOutputStream),
        printRootPkg = false
      ) {
        def apply(decodedName: pressy.Name): String = printedName(decodedName, decoded = true)
      }

      (member: pressy.Member) => {
        import pressy._
        // nsc returns certain members w/ a suffix (LOCAL_SUFFIX_STRING, " ").
        // See usages of symNameDropLocal in nsc's PresentationCompilerCompleter.
        // Several people have asked that Scala mask this implementation detail:
        // https://github.com/scala/bug/issues/5736
        val decodedName = member.sym.name.dropLocal.decodedName
        backQuoter(decodedName)
      }
    }

    val r = new Response[pressy.Tree]
    pressy.askTypeAt(new OffsetPosition(currentFile, index), r)
    val tree = r.get.fold(x => x, e => throw e)

    /**
     * Search for terms to autocomplete not just from the local scope,
     * but from any packages and package objects accessible from the
     * local scope
     */
    def deepCompletion(name: String) = {
      def rec(t: pressy.Symbol): Seq[pressy.Symbol] = {
        if (blacklistedPackages(t.nameString))
          Nil
        else {
          val children =
            if (t.hasPackageFlag || t.isPackageObject) {
              pressy.ask(() => t.typeSignature.members.filter(_ != t).flatMap(rec))
            } else Nil

          t +: children.toSeq
        }
      }

      for {
        member <- pressy.RootClass.typeSignature.members.toList
        sym <- rec(member)
        // sketchy name munging because I don't know how to do this properly
        // Note lack of back-quoting support.
        strippedName = sym.nameString.stripPrefix("package$").stripSuffix("$")
        if strippedName.startsWith(name)
        (pref, _) = sym.fullNameString.splitAt(sym.fullNameString.lastIndexOf('.') + 1)
        out = pref + strippedName
        if out != ""
      } yield (out, None)
    }
    def handleTypeCompletion(position: Int, decoded: String, offset: Int) = {

      val r = ask(position, pressy.askTypeCompletion)
      val prefix = if (decoded == "") "" else decoded
      (position + offset, handleCompletion(r, prefix))
    }

    def handleCompletion(r: List[pressy.Member], prefix: String) = pressy.ask { () =>
      r.filter(_.sym.name.decoded.startsWith(prefix))
        .filter(m => !blacklisted(m.sym))
        .map { x =>
          (
            memberToString(x),
            if (x.sym.name.decoded != prefix) None
            else Some(x.sym.defString)
          )
        }
    }

    def prefixed: (Int, Seq[(String, Option[String])]) = tree match {
      case t @ pressy.Select(qualifier, name) =>
        val dotOffset = if (qualifier.pos.point == t.pos.point) 0 else 1

        // In scala 2.10.x if we call pos.end on a scala.reflect.internal.util.Position
        // that is not a range, a java.lang.UnsupportedOperationException is thrown.
        // We check here if Position is a range before calling .end on it.
        // This is not needed for scala 2.11.x.
        if (qualifier.pos.isRange) {
          handleTypeCompletion(qualifier.pos.end, name.decoded, dotOffset)
        } else {
          // not prefixed
          (0, Seq.empty)
        }

      case t @ pressy.Import(expr, selectors) =>
        // If the selectors haven't been defined yet...
        if (selectors.head.name.toString == "") {
          if (expr.tpe.toString == "") {
            // If the expr is badly typed, try to scope complete it
            if (expr.isInstanceOf[pressy.Ident]) {
              val exprName = expr.asInstanceOf[pressy.Ident].name.decoded
              val pos =
                // Without the first case, things like `import ` are
                // returned a wrong position.
                if (exprName == "") expr.pos.point - 1
                else expr.pos.point
              pos -> handleCompletion(
                ask(expr.pos.point, pressy.askScopeCompletion),
                // if it doesn't have a name at all, accept anything
                if (exprName == "") "" else exprName
              )
            } else (expr.pos.point, Seq.empty)
          } else {
            // If the expr is well typed, type complete
            // the next thing
            handleTypeCompletion(expr.pos.end, "", 1)
          }
        } else {
          val isImportIvy = expr.isInstanceOf[pressy.Ident] &&
            expr.asInstanceOf[pressy.Ident].name.decoded == "$ivy"
          val selector = selectors
            .filter(s => Math.max(s.namePos, s.renamePos) <= index)
            .lastOption
            .getOrElse(selectors.last)

          if (isImportIvy) {
            def forceOpenedBacktick(s: String): String = {
              val res = Name(s).backticked
              if (res.startsWith("`")) res.stripSuffix("`")
              else "`" + res
            }
            dependencyCompleteOpt match {
              case None => 0 -> Seq.empty[(String, Option[String])]
              case Some(complete) =>
                val input = selector.name.decoded
                val (pos, completions) = complete(input)
                val input0 = input.take(pos)
                (selector.namePos, completions.map(s => forceOpenedBacktick(input0 + s) -> None))
            }
          } else
            // just use typeCompletion
            handleTypeCompletion(selector.namePos, selector.name.decoded, 0)
        }
      case t @ pressy.Ident(name) =>
        lazy val shallow = handleCompletion(
          ask(index, pressy.askScopeCompletion),
          name.decoded
        )
        lazy val deep = deepCompletion(name.decoded).distinct

        val res =
          if (shallow.length > 0) shallow
          else if (deep.length == 1) deep
          else deep :+ ("" -> None)

        (t.pos.start, res)

      case t =>
        val comps = ask(index, pressy.askScopeCompletion)

        index -> pressy.ask(() =>
          comps.filter(m => !blacklisted(m.sym))
            .map { s => (memberToString(s), None) }
        )
    }
    def ask(index: Int, query: (Position, Response[List[pressy.Member]]) => Unit) = {
      val position = new OffsetPosition(currentFile, index)
      // if a match can't be found awaitResponse throws an Exception.
      val result = Try(Compiler.awaitResponse[List[pressy.Member]](query(position, _)))
      result match {
        case Success(scopes) => scopes.filter(_.accessible)
        case Failure(error) => List.empty[pressy.Member]
      }
    }

  }
  def apply(
      classpath: Seq[java.net.URL],
      dynamicClasspath: VirtualDirectory,
      evalClassloader: => ClassLoader,
      settings: Settings,
      dependencyCompleteOpt: => Option[String => (Int, Seq[String])],
      classPathWhitelist: Set[Seq[String]],
      initialClassPath: Seq[java.net.URL]
  ): Pressy = new Pressy {

    @volatile var cachedPressy: nsc.interactive.Global = null

    def compiler = {
      if (cachedPressy == null) cachedPressy = initPressy
      cachedPressy
    }
    def initInteractiveGlobal(
        settings: Settings,
        reporter: Reporter,
        jcp: AggregateClassPath,
        evalClassloader: ClassLoader
    ): InteractiveGlobal = {
      new nsc.interactive.Global(settings, reporter) { g =>
        // Actually jcp, avoiding a path-dependent type issue in 2.10 here
        override def classPath = jcp

        override lazy val platform: ThisPlatform = new GlobalPlatform {
          override val global = g
          override val settings = g.settings
          override val classPath = jcp
        }

        override lazy val analyzer = CompilerCompatibility.interactiveAnalyzer(g, evalClassloader)
      }
    }
    def initPressy = {
      val (dirDeps, jarDeps) = classpath.partition { u =>
        u.getProtocol == "file" &&
        java.nio.file.Files.isDirectory(java.nio.file.Paths.get(u.toURI))
      }
      val jcp = Compiler.initGlobalClasspath(
        dirDeps,
        jarDeps,
        dynamicClasspath,
        settings,
        classPathWhitelist,
        initialClassPath
      )
      val reporter = makeReporter((_, _) => (), (_, _) => (), (_, _) => (), settings)
      initInteractiveGlobal(settings, reporter, jcp, evalClassloader)
    }

    def complete(snippetIndex: Int, previousImports: String, snippet: String) = {
      val prefix = previousImports + newLine + "object AutocompleteWrapper{" + newLine
      val suffix = newLine + "}"
      val allCode = prefix + snippet + suffix
      val index = snippetIndex + prefix.length

      val pressy = compiler
      val currentFile = new BatchSourceFile(
        Compiler.makeFile(allCode.getBytes, name = "Current.sc"),
        allCode
      )

      val r = new Response[Unit]
      pressy.askReload(List(currentFile), r)
      r.get.fold(x => x, e => throw e)

      val run = Try(new Run(pressy, currentFile, dependencyCompleteOpt, allCode, index))

      val (i, all): (Int, Seq[(String, Option[String])]) = run.map(_.prefixed) match {
        case Success(prefixed) => prefixed
        case Failure(throwable) => (0, Seq.empty)
      }

      val allNames = all.collect { case (name, None) => name }.sorted.distinct

      val signatures = all.collect { case (name, Some(defn)) => defn }.sorted.distinct

      val isPartialIvyImport = allNames.isEmpty &&
        snippet.split("\\s+|`").startsWith(Seq("import", "$ivy.")) &&
        snippet.count(_ == '`') == 1

      if (isPartialIvyImport) {
        complete(snippetIndex, previousImports, snippet + "`")
      } else (i - prefix.length, allNames, signatures)
    }

    def shutdownPressy() = {
      Option(cachedPressy).foreach(_.askShutdown())
      cachedPressy = null
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy