polynote.kernel.interpreter.scal.ScalaCompleter.scala Maven / Gradle / Ivy
The newest version!
package polynote.kernel.interpreter.scal
import polynote.kernel.{Completion, CompletionType, ParameterHint, ParameterHints, ScalaCompiler, Signatures}
import polynote.messages.{ShortString, TinyList, TinyString}
import cats.syntax.either._
import zio.{Fiber, RIO, Schedule, Task, UIO, URIO, ZIO}
import ZIO.{effect, effectTotal}
import polynote.kernel.ScalaCompiler.OriginalPos
import polynote.kernel.interpreter.scal.ScalaCompleter.NoTree
import zio.blocking.{Blocking, effectBlocking}
import zio.clock.Clock
import scala.annotation.tailrec
import scala.collection.immutable.TreeMap
import scala.collection.mutable
import scala.reflect.internal.util.Position
import scala.util.control.NonFatal
class ScalaCompleter[Compiler <: ScalaCompiler](
val compiler: Compiler,
index: ClassIndexer
) {
import compiler.global._
private def indexCompletions(search: String) = index.findMatches(search).map {
matches => matches.toList.flatMap {
case (shortName, candidates) => candidates.map {
case (priority, longName) =>
val abbrevPath = longName.split('.').dropRight(1).toList match {
case first :: rest => rest.reverse match {
case last :: middle => (first :: (last :: middle.map(_.head.toString)).reverse).mkString(".")
case Nil => first
}
case Nil => ""
}
priority -> Completion(shortName, Nil, Nil, abbrevPath, CompletionType.Unknown, Some(longName))
}
}.sortBy(_._1).map(_._2)
}
def completions(cellCode: compiler.CellCode, pos: Int): URIO[Blocking, List[Completion]] = {
val position = Position.offset(cellCode.sourceFile, pos)
//lazy val importInfos = (cellCode.wrappedImports ++ cellCode.compiledImports).map(new analyzer.ImportInfo(_, 1))
def symToCompletion(sym: Symbol, inType: Type) = {
val name = sym.name.decodedName.toString.trim // term names seem to get an extra space at the end?
val typ = sym.typeSignatureIn(inType).resultType
val tParams = sym.typeParams.map(_.name.decodedName.toString)
val vParams = TinyList(sym.paramss.map(_.map{p => (p.name.decodedName.toString: TinyString, compiler.unsafeFormatType(p.infoIn(typ)): ShortString)}: TinyList[(TinyString, ShortString)]))
Completion(name, tParams, vParams, compiler.unsafeFormatType(typ), completionType(sym))
}
def memberToCompletion(result: Member) = {
symToCompletion(result.sym, result.tpe).copy(resultType = compiler.unsafeFormatType(result.tpe.resultType))
}
def completeSelect(tree: Select) = tree match {
case sel@Select(qual, name) if qual.tpe != null =>
val isErrorOrEmpty = name.decoded == "" || name.isEmpty
effectBlocking {
val context = locateContext(position).getOrElse(NoContext)
val fromCompiler = typeCompletions(context, sel)
fromCompiler.results.filter {
result => result.accessible && (isErrorOrEmpty || result.sym.name.startsWith(name))
}
}.map {
results =>
results.map(memberToCompletion).distinct
}
case _ =>
ZIO.succeed(Nil)
}
def completeIdent(tree: Ident) =
effectBlocking(compiler.global.completionsAt(position)).map {
result =>
result.matchingResults().map(memberToCompletion).distinct
}
def completeImport(tree: Import) = tree match {
case Import(qual, names) if qual.tpe != null && !qual.tpe.isError =>
val searchName = names.dropWhile(_.namePos < pos).headOption match {
case None => TermName("")
case Some(sel) if sel.name.decoded == "" => TermName("")
case Some(sel) => sel.name
}
def isImportable(sym: Symbol): Boolean =
isVisibleSymbol(sym) && (sym.decodedName == sym.encodedName) && sym.name.startsWith(searchName)
ZIO {
qual.tpe.members.filter(isImportable)
.toList
.sorted(importOrdering)
.groupBy(_.name.decoded).values.map(_.head).toList
.map(symToCompletion(_, NoType))
}
case Import(Ident(name), List(ImportSelector(TermName(""), _, _, _))) =>
val rootPackages = ZIO {
val symbols = compiler.global.rootMirror.RootPackage.info.members.collect {
case sym: ModuleSymbol if !sym.isRootPackage && !sym.isEmptyPackage && sym.hasPackageFlag && sym.name.startsWith(name) => sym
}
symbols.toList.map {
sym =>
Completion(sym.name.toString, Nil, Nil, "", CompletionType.Package)
}
}
val fromIndex = indexCompletions(name.toString)
(rootPackages <&> fromIndex).map {
case (rootPackages, fromIndex) => rootPackages ++ fromIndex
}
case _ => ZIO.succeed(Nil)
}
def completeApply(tree: Apply) = tree match {
case Apply(fun, args) =>
args.collectFirst {
case arg if arg.pos != null && arg.pos.isDefined && arg.pos.includes(position) => arg
} match {
case Some(arg) => completeTree(arg)
case None => fun match {
case Select(tree@New(_), TermName("")) => completeTree(tree.tpt)
case tree => completeTree(tree)
}
}
}
def completeNew(original: Tree) = {
val fromIndex = original match {
case Ident(name) => indexCompletions(name.toString)
case _ => ZIO.succeed(Nil)
}
ZIO.mapN(completeTree(original), fromIndex)(_ ++ _)
}
@tailrec def completeTree(tree: Tree): RIO[Blocking, List[Completion]] = tree match {
case tree@Select(qual, _) if qual != null => completeSelect(tree)
case tree@Ident(_) => completeIdent(tree)
case tree@Import(_, _) => completeImport(tree)
case tree@Apply(_, _) => completeApply(tree)
case New(tpt@TypeTree()) if tpt.original != null => completeNew(tpt.original)
case New(tpt) => completeTree(tpt)
case tree@TypeTree() if tree.original != null => completeTree(tree.original)
case other =>
val o = other
ZIO.succeed(Nil)
}
cellCode.typedTreeAt(pos).filterOrFail(_ != EmptyTree)(NoTree).retryN(2).flatMap(completeTree).catchAll {
case NonFatal(err) => ZIO.succeed(Nil)
}
}
private val importOrdering: Ordering[Symbol] = new Ordering[Symbol] {
def compare(x: Symbol, y: Symbol): Int = Ordering.Boolean.compare(y.hasPackageFlag, x.hasPackageFlag) match {
case 0 => Ordering.Int.compare(y.ownerChain.length, x.ownerChain.length) match {
case 0 => Ordering.Boolean.compare(x.name.isOperatorName, y.name.isOperatorName) match {
case 0 => Ordering.String.compare(x.name.decoded, y.name.decoded)
case s => s
}
case s => s
}
case s => s
}
}
// TODO: using the typedTreeAt() is an improvement overall, but there's still an issue where the signature help will
// disappear when you have a valid tree in parameter position (because now the smallest tree is that tree, not)
// the apply). Should try to back up and locate the smallest apply tree instead, when that fails. Compilation
// unit should contain the typed trees necessary to do it.
def paramHints(cellCode: compiler.CellCode, pos: Int): URIO[Blocking, Option[Signatures]] = cellCode.typedTreeAt(pos).flatMap {
typedCode =>
effect {
applyTreeAt(typedCode, pos).map {
case a@Apply(fun, args) =>
val (paramList, prevArgs, outerApply) = whichParamList(a, 0, 0)
val whichArg = math.max(args.size - 1, 0)
def methodHints(method: MethodSymbol) = {
val paramsStr = method.paramLists.map {
pl => "(" + pl.map {
param => s"${param.name.decodedName.toString}: ${param.typeSignatureIn(a.tpe).finalResultType.toString}"
}.mkString(", ") + ")"
}.mkString
val params = method.paramLists.flatMap {
pl => pl.map {
param => ParameterHint(
TinyString(param.name.decodedName.toString),
TinyString(param.typeSignatureIn(fun.tpe).finalResultType.toString),
None // TODO
)
}
}
List(ParameterHints(
method.name.decodedName.toString + paramsStr,
None,
params
))
}
val hints = fun.symbol match {
case null => Nil
case err if err.isError =>
fun match {
case Select(qual, name) if !qual.isErrorTyped => qual.tpe.member(name) match {
case sym if sym.isMethod =>
methodHints(sym.asMethod)
case sym if sym.isTerm && sym.isOverloaded =>
sym.asTerm.alternatives.collect {
case sym if sym.isMethod => methodHints(sym.asMethod)
}.flatten
case other =>
Nil
}
case other =>
Nil
}
case method if method.isMethod => methodHints(method.asMethod)
case _ => Nil
}
Signatures(hints, 0, (prevArgs + whichArg).toByte)
}
}
}.option.map(_.flatten)
@tailrec
private def whichParamList(tree: Apply, n: Int, nArgs: Int): (Int, Int, Apply) = tree.fun match {
case a@Apply(_, args) => whichParamList(a, n + 1, nArgs + args.size)
case _ => (n, nArgs, tree)
}
private def isVisibleSymbol(sym: Symbol) =
sym.isPublic && !sym.isSynthetic && !sym.isConstructor && !sym.isOmittablePrefix && !sym.name.decodedName.containsChar('$')
// the compiler's completionsAt method has a bug; it causes an exception if the name is empty
// (e.g. `foo.`) which is a pretty common case. So there will be a fair amount of copypasta here,
// as the component methods we'd need are all private. This bit is copied from interactive.Global#typeMembers
private def typeMembers(context: Context, tree: Tree) = {
// had to copypasta this class as well, since it's private.
class Members[M <: Member] extends mutable.LinkedHashMap[Name, Set[M]] {
import scala.reflect.internal.Flags._
override def default(key: Name) = Set()
private def matching(sym: Symbol, symtpe: Type, ms: Set[M]): Option[M] = ms.find { m =>
(m.sym.name == sym.name) && (m.sym.isType || (m.tpe matches symtpe))
}
private def keepSecond(m: M, sym: Symbol, implicitlyAdded: Boolean): Boolean =
m.sym.hasFlag(ACCESSOR | PARAMACCESSOR) &&
!sym.hasFlag(ACCESSOR | PARAMACCESSOR) &&
(!implicitlyAdded || m.implicitlyAdded)
def add(sym: Symbol, pre: Type, implicitlyAdded: Boolean)(toMember: (Symbol, Type) => M): Unit = {
if ((sym.isGetter || sym.isSetter) && sym.accessed != NoSymbol) {
add(sym.accessed, pre, implicitlyAdded)(toMember)
} else if (!sym.name.decodedName.containsName("$") && !sym.isError && !sym.isArtifact && sym.hasRawInfo) {
val symtpe = pre.memberType(sym) onTypeError ErrorType
matching(sym, symtpe, this(sym.name)) match {
case Some(m) =>
if (keepSecond(m, sym, implicitlyAdded)) {
//print(" -+ "+sym.name)
this(sym.name) = this(sym.name) - m + toMember(sym, symtpe)
}
case None =>
//print(" + "+sym.name)
this(sym.name) = this(sym.name) + toMember(sym, symtpe)
}
}
}
def addNonShadowed(other: Members[M]): Unit = {
for ((name, ms) <- other)
if (ms.nonEmpty && this(name).isEmpty) this(name) = ms
}
def allMembers: List[M] = values.toList.flatten
}
val superAccess = tree.isInstanceOf[Super]
val members = new Members[TypeMember]
def addTypeMember(sym: Symbol, pre: Type, inherited: Boolean, viaView: Symbol): Unit = {
val implicitlyAdded = viaView != NoSymbol
members.add(sym, pre, implicitlyAdded) { (s, st) =>
val result = new TypeMember(s, st,
context.isAccessible(if (s.hasGetter) s.getterIn(s.owner) else s, pre, superAccess && !implicitlyAdded),
inherited,
viaView)
result.prefix = pre
result
}
}
import analyzer.{SearchResult, ImplicitSearch}
import definitions.{functionType, AnyTpe}
/** Create a function application of a given view function to `tree` and typechecked it.
*/
def viewApply(view: SearchResult): Tree = {
assert(view.tree != EmptyTree)
analyzer.newTyper(context.makeImplicit(reportAmbiguousErrors = false))
.typed(Apply(view.tree, List(tree)) setPos tree.pos)
.onTypeError(EmptyTree)
}
val pre = stabilizedType(tree)
val ownerTpe = tree.tpe match {
case ImportType(expr) => expr.tpe
case null => pre
case MethodType(List(), rtpe) => rtpe
case _ => tree.tpe
}
//print("add members")
for (sym <- ownerTpe.members)
addTypeMember(sym, pre, sym.owner != ownerTpe.typeSymbol, NoSymbol)
members.allMembers #:: {
//print("\nadd enrichment")
val applicableViews: List[SearchResult] =
if (ownerTpe.isErroneous) List()
else new ImplicitSearch(
tree, functionType(List(ownerTpe), AnyTpe), isView = true,
context0 = context.makeImplicit(reportAmbiguousErrors = false)).allImplicits
for (view <- applicableViews) {
val vtree = viewApply(view)
val vpre = stabilizedType(vtree)
for (sym <- vtree.tpe.members if sym.isTerm) {
addTypeMember(sym, vpre, inherited = false, view.tree.symbol)
}
}
//println()
Stream(members.allMembers)
}
}
// copied from inner method typeCompletions in interactive.Global#completionsAt. But without fatal bug.
private def typeCompletions(context: Context, tree: Select) = {
val allTypeMembers = typeMembers(context, tree.qualifier).toList.flatten
CompletionResult.TypeMembers(0, tree.qualifier, tree, allTypeMembers, tree.name)
}
private def applyTreeAt(tree: Tree, offset: Int): Option[Apply] = tree.collect {
case a: Apply if a.pos != null && a.pos.isOpaqueRange && a.pos.start <= offset && a.pos.end >= offset =>
a
}.lastOption
def completionType(sym: Symbol): CompletionType =
if (sym.isAccessor)
CompletionType.Field
else if (sym.isMethod)
CompletionType.Method
else if (sym.isPackageObjectOrClass)
CompletionType.Package
else if (sym.isTrait)
CompletionType.TraitType
else if (sym.isModule)
CompletionType.Module
else if (sym.isClass)
CompletionType.ClassType
else if (sym.isVariable || sym.isVal)
CompletionType.Term
else
CompletionType.Unknown
}
object ScalaCompleter {
object NoTree extends Throwable("No typed tree found")
def apply(compiler: ScalaCompiler, indexer: ClassIndexer): ScalaCompleter[compiler.type] = new ScalaCompleter(compiler, indexer)
}