diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala new file mode 100644 index 000000000000..5fa83ef229df --- /dev/null +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -0,0 +1,304 @@ +package dotty.tools.dotc.interactive + +import dotty.tools.dotc.ast.Trees._ +import dotty.tools.dotc.config.Printers.interactiv +import dotty.tools.dotc.core.Contexts.{Context, NoContext} +import dotty.tools.dotc.core.CheckRealizable +import dotty.tools.dotc.core.Decorators.StringInterpolators +import dotty.tools.dotc.core.Denotations.SingleDenotation +import dotty.tools.dotc.core.Flags._ +import dotty.tools.dotc.core.Names.{Name, TermName} +import dotty.tools.dotc.core.NameKinds.SimpleNameKind +import dotty.tools.dotc.core.NameOps.NameDecorator +import dotty.tools.dotc.core.Symbols.{defn, NoSymbol, Symbol} +import dotty.tools.dotc.core.Scopes +import dotty.tools.dotc.core.StdNames.{nme, tpnme} +import dotty.tools.dotc.core.TypeError +import dotty.tools.dotc.core.Types.{NamedType, Type, takeAllFilter} +import dotty.tools.dotc.printing.Texts._ +import dotty.tools.dotc.util.{NoSourcePosition, SourcePosition} + +import scala.collection.mutable + +object Completion { + + import dotty.tools.dotc.ast.tpd._ + + /** Get possible completions from tree at `pos` + * + * @return offset and list of symbols for possible completions + */ + def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = { + val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.pos) + computeCompletions(pos, path)(Interactive.contextOfPath(path)) + } + + /** + * Inspect `path` to determine what kinds of symbols should be considered. + * + * If the path starts with: + * - a `RefTree`, then accept symbols of the same kind as its name; + * - a renaming import, and the cursor is on the renamee, accept both terms and types; + * - an import, accept both terms and types; + * + * Otherwise, provide no completion suggestion. + */ + private def completionMode(path: List[Tree], pos: SourcePosition): Mode = { + path match { + case (ref: RefTree) :: _ => + if (ref.name.isTermName) Mode.Term + else if (ref.name.isTypeName) Mode.Type + else Mode.None + + case Thicket(name :: _ :: Nil) :: (_: Import) :: _ => + if (name.pos.contains(pos.pos)) Mode.Import + else Mode.None // Can't help completing the renaming + + case Import(_, _) :: _ => + Mode.Import + + case _ => + Mode.None + } + } + + /** + * Inspect `path` to determine the completion prefix. Only symbols whose name start with the + * returned prefix should be considered. + */ + private def completionPrefix(path: List[Tree], pos: SourcePosition): String = { + path match { + case Thicket(name :: _ :: Nil) :: (_: Import) :: _ => + completionPrefix(name :: Nil, pos) + + case Import(expr, selectors) :: _ => + selectors.find(_.pos.contains(pos.pos)).map { selector => + completionPrefix(selector.asInstanceOf[Tree] :: Nil, pos) + }.getOrElse("") + + case (ref: RefTree) :: _ => + if (ref.name == nme.ERROR) "" + else ref.name.toString.take(pos.pos.point - ref.pos.point) + + case _ => + "" + } + } + + /** Inspect `path` to determine the offset where the completion result should be inserted. */ + private def completionOffset(path: List[Tree]): Int = { + path match { + case (ref: RefTree) :: _ => ref.pos.point + case _ => 0 + } + } + + /** Create a new `CompletionBuffer` for completing at `pos`. */ + private def completionBuffer(path: List[Tree], pos: SourcePosition): CompletionBuffer = { + val mode = completionMode(path, pos) + val prefix = completionPrefix(path, pos) + new CompletionBuffer(mode, prefix, pos) + } + + private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Symbol]) = { + + val offset = completionOffset(path) + val buffer = completionBuffer(path, pos) + + if (buffer.mode != Mode.None) { + path match { + case Select(qual, _) :: _ => buffer.addMemberCompletions(qual) + case Import(expr, _) :: _ => buffer.addMemberCompletions(expr) + case (_: Thicket) :: Import(expr, _) :: _ => buffer.addMemberCompletions(expr) + case _ => buffer.addScopeCompletions + } + } + + val completionList = buffer.getCompletions + + interactiv.println(i"""completion with pos = $pos, + | prefix = ${buffer.prefix}, + | term = ${buffer.mode.is(Mode.Term)}, + | type = ${buffer.mode.is(Mode.Type)} + | results = $completionList%, %""") + (offset, completionList) + } + + private class CompletionBuffer(val mode: Mode, val prefix: String, pos: SourcePosition) { + + private[this] val completions = Scopes.newScope.openForMutations + + /** + * Return the list of symbols that shoudl be included in completion results. + * + * If the mode is `Import` and several symbols share the same name, the type symbols are + * preferred over term symbols. + */ + def getCompletions(implicit ctx: Context): List[Symbol] = { + // Show only the type symbols when there are multiple options with the same name + completions.toList.groupBy(_.name.stripModuleClassSuffix.toSimpleName).mapValues { + case sym :: Nil => sym :: Nil + case syms => syms.filter(_.isType) + }.values.flatten.toList + } + + /** + * Add symbols that are currently in scope to `info`: the members of the current class and the + * symbols that have been imported. + */ + def addScopeCompletions(implicit ctx: Context): Unit = { + if (ctx.owner.isClass) { + addAccessibleMembers(ctx.owner.thisType) + ctx.owner.asClass.classInfo.selfInfo match { + case selfSym: Symbol => add(selfSym) + case _ => + } + } + else if (ctx.scope != null) ctx.scope.foreach(add) + + addImportCompletions + + var outer = ctx.outer + while ((outer.owner `eq` ctx.owner) && (outer.scope `eq` ctx.scope)) { + addImportCompletions(outer) + outer = outer.outer + } + if (outer `ne` NoContext) addScopeCompletions(outer) + } + + /** + * Find all the members of `qual` and add the ones that pass the include filters to `info`. + * + * If `info.mode` is `Import`, the members added via implicit conversion on `qual` are not + * considered. + */ + def addMemberCompletions(qual: Tree)(implicit ctx: Context): Unit = { + addAccessibleMembers(qual.tpe) + if (!mode.is(Mode.Import)) { + // Implicit conversions do not kick in when importing + implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState()) + .foreach(addAccessibleMembers) + } + } + + /** + * If `sym` exists, no symbol with the same name is already included, and it satisfies the + * inclusion filter, then add it to the completions. + */ + private def add(sym: Symbol)(implicit ctx: Context) = + if (sym.exists && !completions.lookup(sym.name).exists && include(sym)) { + completions.enter(sym) + } + + /** Lookup members `name` from `site`, and try to add them to the completion list. */ + private def addMember(site: Type, name: Name)(implicit ctx: Context) = + if (!completions.lookup(name).exists) + for (alt <- site.member(name).alternatives) add(alt.symbol) + + /** Include in completion sets only symbols that + * 1. start with given name prefix, and + * 2. do not contain '$' except in prefix where it is explicitly written by user, and + * 3. are not a primary constructor, + * 4. are the module class in case of packages, + * 5. are mutable accessors, to exclude setters for `var`, + * 6. have same term/type kind as name prefix given so far + * + * The reason for (2) is that we do not want to present compiler-synthesized identifiers + * as completion results. However, if a user explicitly writes all '$' characters in an + * identifier, we should complete the rest. + */ + private def include(sym: Symbol)(implicit ctx: Context): Boolean = + sym.name.startsWith(prefix) && + !sym.name.toString.drop(prefix.length).contains('$') && + !sym.isPrimaryConstructor && + (!sym.is(Package) || !sym.moduleClass.exists) && + !sym.is(allOf(Mutable, Accessor)) && + ( + (mode.is(Mode.Term) && sym.isTerm) + || (mode.is(Mode.Type) && (sym.isType || sym.isStable)) + ) + + /** + * Find all the members of `site` that are accessible and which should be included in `info`. + * + * @param site The type to inspect. + * @return The members of `site` that are accessible and pass the include filter of `info`. + */ + private def accessibleMembers(site: Type)(implicit ctx: Context): Seq[Symbol] = site match { + case site: NamedType if site.symbol.is(Package) => + site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive. + case _ => + def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit = + try buf ++= site.member(name).alternatives + catch { case ex: TypeError => } + site.memberDenots(takeAllFilter, appendMemberSyms).collect { + case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess = true).symbol + case _ => NoSymbol + }.filter(_.exists) + } + + /** Add all the accessible members of `site` in `info`. */ + private def addAccessibleMembers(site: Type)(implicit ctx: Context): Unit = + for (mbr <- accessibleMembers(site)) addMember(site, mbr.name) + + /** + * Add in `info` the symbols that are imported by `ctx.importInfo`. If this is a wildcard import, + * all the accessible members of the import's `site` are included. + */ + private def addImportCompletions(implicit ctx: Context): Unit = { + val imp = ctx.importInfo + if (imp != null) { + def addImport(name: TermName) = { + addMember(imp.site, name) + addMember(imp.site, name.toTypeName) + } + // FIXME: We need to also take renamed items into account for completions, + // That means we have to return list of a pairs (Name, Symbol) instead of a list + // of symbols from `completions`.!= + for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported) + if (imp.isWildcardImport) + for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName)) + addMember(imp.site, mbr.name) + } + } + + /** + * Given `qual` of type T, finds all the types S such that there exists an implicit conversion + * from T to S. + * + * @param qual The argument to which the implicit conversion should be applied. + * @return The set of types that `qual` can be converted to. + */ + private def implicitConversionTargets(qual: Tree)(implicit ctx: Context): Set[Type] = { + val typer = ctx.typer + val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.pos).allImplicits + val targets = conversions.map(_.widen.finalResultType) + interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %") + targets + } + + } + + /** + * The completion mode: defines what kinds of symbols should be included in the completion + * results. + */ + private class Mode(val bits: Int) extends AnyVal { + def is(other: Mode): Boolean = (bits & other.bits) == other.bits + def |(other: Mode): Mode = new Mode(bits | other.bits) + } + private object Mode { + /** No symbol should be included */ + val None: Mode = new Mode(0) + + /** Term symbols are allowed */ + val Term: Mode = new Mode(1) + + /** Type and stable term symbols are allowed */ + val Type: Mode = new Mode(2) + + /** Both term and type symbols are allowed */ + val Import: Mode = new Mode(4) | Term | Type + } + +} diff --git a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala index 77b0b5ec134c..419e82dfa2fb 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala @@ -151,159 +151,6 @@ object Interactive { ) } - private def safely[T](op: => List[T]): List[T] = - try op catch { case ex: TypeError => Nil } - - /** Get possible completions from tree at `pos` - * - * @return offset and list of symbols for possible completions - */ - def completions(pos: SourcePosition)(implicit ctx: Context): (Int, List[Symbol]) = { - val path = pathTo(ctx.compilationUnit.tpdTree, pos.pos) - computeCompletions(pos, path)(contextOfPath(path)) - } - - private def computeCompletions(pos: SourcePosition, path: List[Tree])(implicit ctx: Context): (Int, List[Symbol]) = { - val completions = Scopes.newScope.openForMutations - - val (completionPos, prefix, termOnly, typeOnly) = path match { - case (ref: RefTree) :: _ => - if (ref.name == nme.ERROR) - (ref.pos.point, "", false, false) - else - (ref.pos.point, - ref.name.toString.take(pos.pos.point - ref.pos.point), - ref.name.isTermName, - ref.name.isTypeName) - case _ => - (0, "", false, false) - } - - /** Include in completion sets only symbols that - * 1. start with given name prefix, and - * 2. do not contain '$' except in prefix where it is explicitly written by user, and - * 3. have same term/type kind as name prefix given so far - * - * The reason for (2) is that we do not want to present compiler-synthesized identifiers - * as completion results. However, if a user explicitly writes all '$' characters in an - * identifier, we should complete the rest. - */ - def include(sym: Symbol) = - sym.name.startsWith(prefix) && - !sym.name.toString.drop(prefix.length).contains('$') && - (!termOnly || sym.isTerm) && - (!typeOnly || sym.isType) - - def enter(sym: Symbol) = - if (include(sym)) completions.enter(sym) - - def add(sym: Symbol) = - if (sym.exists && !completions.lookup(sym.name).exists) enter(sym) - - def addMember(site: Type, name: Name) = - if (!completions.lookup(name).exists) - for (alt <- site.member(name).alternatives) enter(alt.symbol) - - def accessibleMembers(site: Type, superAccess: Boolean = true): Seq[Symbol] = site match { - case site: NamedType if site.symbol.is(Package) => - site.decls.toList.filter(include) // Don't look inside package members -- it's too expensive. - case _ => - def appendMemberSyms(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit = - try buf ++= site.member(name).alternatives - catch { case ex: TypeError => } - site.memberDenots(takeAllFilter, appendMemberSyms).collect { - case mbr if include(mbr.symbol) => mbr.accessibleFrom(site, superAccess).symbol - case _ => NoSymbol - }.filter(_.exists) - } - - def addAccessibleMembers(site: Type, superAccess: Boolean = true): Unit = - for (mbr <- accessibleMembers(site)) addMember(site, mbr.name) - - def getImportCompletions(ictx: Context): Unit = { - implicit val ctx = ictx - val imp = ctx.importInfo - if (imp != null) { - def addImport(name: TermName) = { - addMember(imp.site, name) - addMember(imp.site, name.toTypeName) - } - // FIXME: We need to also take renamed items into account for completions, - // That means we have to return list of a pairs (Name, Symbol) instead of a list - // of symbols from `completions`.!= - for (imported <- imp.originals if !imp.excluded.contains(imported)) addImport(imported) - if (imp.isWildcardImport) - for (mbr <- accessibleMembers(imp.site) if !imp.excluded.contains(mbr.name.toTermName)) - addMember(imp.site, mbr.name) - } - } - - def getScopeCompletions(ictx: Context): Unit = { - implicit val ctx = ictx - - if (ctx.owner.isClass) { - addAccessibleMembers(ctx.owner.thisType) - ctx.owner.asClass.classInfo.selfInfo match { - case selfSym: Symbol => add(selfSym) - case _ => - } - } - else if (ctx.scope != null) ctx.scope.foreach(add) - - getImportCompletions(ctx) - - var outer = ctx.outer - while ((outer.owner `eq` ctx.owner) && (outer.scope `eq` ctx.scope)) { - getImportCompletions(outer) - outer = outer.outer - } - if (outer `ne` NoContext) getScopeCompletions(outer) - } - - def implicitConversionTargets(qual: Tree)(implicit ctx: Context): Set[Type] = { - val typer = ctx.typer - val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.pos).allImplicits - val targets = conversions.map(_.widen.finalResultType) - interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %") - targets - } - - def getMemberCompletions(qual: Tree): Unit = { - addAccessibleMembers(qual.tpe) - implicitConversionTargets(qual)(ctx.fresh.setExploreTyperState()) - .foreach(addAccessibleMembers(_)) - } - - path match { - case (sel @ Select(qual, _)) :: _ => getMemberCompletions(qual) - case _ => getScopeCompletions(ctx) - } - - val completionList = completions.toList - interactiv.println(i"completion with pos = $pos, prefix = $prefix, termOnly = $termOnly, typeOnly = $typeOnly = $completionList%, %") - (completionPos, completionList) - } - - /** Possible completions of members of `prefix` which are accessible when called inside `boundary` */ - def completions(prefix: Type, boundary: Symbol)(implicit ctx: Context): List[Symbol] = - safely { - if (boundary != NoSymbol) { - val boundaryCtx = ctx.withOwner(boundary) - def exclude(sym: Symbol) = sym.isAbsent || sym.is(Synthetic) || sym.is(Artifact) - def addMember(name: Name, buf: mutable.Buffer[SingleDenotation]): Unit = - buf ++= prefix.member(name).altsWith(sym => - !exclude(sym) && sym.isAccessibleFrom(prefix)(boundaryCtx)) - prefix.memberDenots(completionsFilter, addMember).map(_.symbol).toList - } - else Nil - } - - /** Filter for names that should appear when looking for completions. */ - private[this] object completionsFilter extends NameFilter { - def apply(pre: Type, name: Name)(implicit ctx: Context): Boolean = - !name.isConstructorName && name.toTermName.info.kind == SimpleNameKind - } - /** Find named trees with a non-empty position whose symbol match `sym` in `trees`. * * Note that nothing will be found for symbols not defined in source code, @@ -572,4 +419,7 @@ object Interactive { n0.stripModuleClassSuffix.toTermName eq n1.stripModuleClassSuffix.toTermName } + private[interactive] def safely[T](op: => List[T]): List[T] = + try op catch { case ex: TypeError => Nil } + } diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 3de64529da88..1c219639201b 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -13,7 +13,7 @@ import dotty.tools.dotc.core.NameOps._ import dotty.tools.dotc.core.Names.Name import dotty.tools.dotc.core.StdNames._ import dotty.tools.dotc.core.Symbols.{Symbol, defn} -import dotty.tools.dotc.interactive.Interactive +import dotty.tools.dotc.interactive.Completion import dotty.tools.dotc.printing.SyntaxHighlighting import dotty.tools.dotc.reporting.MessageRendering import dotty.tools.dotc.reporting.diagnostic.{Message, MessageContainer} @@ -170,7 +170,7 @@ class ReplDriver(settings: Array[String], unit.tpdTree = tree implicit val ctx = state.context.fresh.setCompilationUnit(unit) val srcPos = SourcePosition(file, Position(cursor)) - val (_, completions) = Interactive.completions(srcPos) + val (_, completions) = Completion.completions(srcPos) completions.map(makeCandidate) } .getOrElse(Nil) diff --git a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala index d4d38f38a4d5..c6abf045c0db 100644 --- a/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala +++ b/language-server/src/dotty/tools/languageserver/DottyLanguageServer.scala @@ -19,6 +19,7 @@ import scala.io.Codec import dotc._ import ast.{Trees, tpd} import core._, core.Decorators.{sourcePos => _, _} +import Annotations.AnnotInfo import Comments._, Constants._, Contexts._, Flags._, Names._, NameOps._, Symbols._, SymDenotations._, Trees._, Types._ import classpath.ClassPathEntries import reporting._, reporting.diagnostic.{Message, MessageContainer, messages} @@ -280,7 +281,7 @@ class DottyLanguageServer extends LanguageServer val pos = sourcePosition(driver, uri, params.getPosition) val items = driver.compilationUnits.get(uri) match { - case Some(unit) => Interactive.completions(pos)(ctx.fresh.setCompilationUnit(unit))._2 + case Some(unit) => Completion.completions(pos)(ctx.fresh.setCompilationUnit(unit))._2 case None => Nil } @@ -779,7 +780,7 @@ object DottyLanguageServer { def completionItemKind(sym: Symbol)(implicit ctx: Context): lsp4j.CompletionItemKind = { import lsp4j.{CompletionItemKind => CIK} - if (sym.is(Package)) + if (sym.is(Package) || sym.is(Module)) CIK.Module // No CompletionItemKind.Package (https://github.com/Microsoft/language-server-protocol/issues/155) else if (sym.isConstructor) CIK.Constructor @@ -795,12 +796,17 @@ object DottyLanguageServer { val label = sym.name.show val item = new lsp4j.CompletionItem(label) - item.setDetail(sym.info.widenTermRefExpr.show) + val detail = if (sym.isType) sym.showFullName else sym.info.widenTermRefExpr.show + item.setDetail(detail) + ParsedComment.docOf(sym).foreach { doc => + item.setDocumentation(markupContent(doc.renderAsMarkdown)) + } + item.setDeprecated(sym.isDeprecated) item.setKind(completionItemKind(sym)) item } - def hoverContent(content: String): lsp4j.MarkupContent = { + def markupContent(content: String): lsp4j.MarkupContent = { if (content.isEmpty) null else { val markup = new lsp4j.MarkupContent @@ -824,7 +830,7 @@ object DottyLanguageServer { buf.append(comment.renderAsMarkdown) } - hoverContent(buf.toString) + markupContent(buf.toString) } /** Create an lsp4j.SymbolInfo from a Symbol and a SourcePosition */ @@ -869,7 +875,7 @@ object DottyLanguageServer { val tparamsLabel = if (signature.tparams.isEmpty) "" else signature.tparams.mkString("[", ", ", "]") val returnTypeLabel = signature.returnType.map(t => s": $t").getOrElse("") val label = s"${signature.name}$tparamsLabel$paramLists$returnTypeLabel" - val documentation = signature.doc.map(DottyLanguageServer.hoverContent) + val documentation = signature.doc.map(DottyLanguageServer.markupContent) val sig = new lsp4j.SignatureInformation(label) sig.setParameters(paramInfoss.flatten.asJava) documentation.foreach(sig.setDocumentation(_)) @@ -878,7 +884,7 @@ object DottyLanguageServer { /** Convert `param` to `ParameterInformation` */ private def paramToParameterInformation(param: Signatures.Param): lsp4j.ParameterInformation = { - val documentation = param.doc.map(DottyLanguageServer.hoverContent) + val documentation = param.doc.map(DottyLanguageServer.markupContent) val info = new lsp4j.ParameterInformation(param.show) documentation.foreach(info.setDocumentation(_)) info diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 1bdca4903ebd..6a4dc94c8d5d 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1,15 +1,17 @@ package dotty.tools.languageserver +import org.junit.Assert.{assertEquals, assertTrue, assertFalse} import org.junit.Test -import org.eclipse.lsp4j.CompletionItemKind +import org.eclipse.lsp4j.CompletionItemKind._ import dotty.tools.languageserver.util.Code._ +import dotty.tools.languageserver.util.actions.CodeCompletion class CompletionTest { @Test def completion0: Unit = { code"class Foo { val xyz: Int = 0; def y: Int = xy$m1 }".withSource - .completion(m1, Set(("xyz", CompletionItemKind.Field, "Int"))) + .completion(m1, Set(("xyz", Field, "Int"))) } @Test def completionWithImplicitConversion: Unit = { @@ -17,6 +19,184 @@ class CompletionTest { code"object Foo { implicit class WithBaz(bar: Bar) { def baz = 0 } }", code"class Bar", code"object Main { import Foo._; val bar: Bar = new Bar; bar.b${m1} }" - ) .completion(m1, Set(("baz", CompletionItemKind.Method, "=> Int"))) + ) .completion(m1, Set(("baz", Method, "=> Int"))) + } + + @Test def importCompleteClassWithPrefix: Unit = { + withSources( + code"""object Foo { class MyClass }""", + code"""import Foo.My${m1}""" + ).completion(m1, Set(("MyClass", Class, "Foo.MyClass"))) + } + + @Test def ImportCompleteClassNoPrefix: Unit = { + withSources( + code"""object Foo { class MyClass }""", + code"""import Foo.${m1}""" + ).completion(m1, completionItems => { + val results = CodeCompletion.simplifyResults(completionItems) + val myClass = ("MyClass", Class, "Foo.MyClass") + assertTrue(results.contains(("MyClass", Class, "Foo.MyClass"))) + + // Verify that apart from `MyClass`, we only have the methods that exists on `Foo` + assertTrue((results - myClass).forall { case (_, kind, _) => kind == Method }) + + // Verify that we don't have things coming from an implicit conversion, such as ensuring + assertFalse(results.exists { case (name, _, _) => name == "ensuring" }) + }) + } + + @Test def importCompleteFromPackage: Unit = { + withSources( + code"""package a + class MyClass""", + code"""package b + import a.My${m1}""" + ).completion(m1, Set(("MyClass", Class, "a.MyClass"))) + } + + @Test def importCompleteFromClass: Unit = { + withSources( + code"""class Foo { val x: Int = 0 }""", + code"""import Foo.${m1}""" + ).completion(m1, Set()) + } + + @Test def importCompleteIncludesSynthetic: Unit = { + code"""case class MyCaseClass(foobar: Int) + object O { + val x = MyCaseClass(0) + import x.c${m1} + }""".withSource + .completion( + m1, + Set(("clone", Method, "(): Object"), + ("copy", Method, "(foobar: Int): MyCaseClass"), + ("canEqual", Method, "(that: Any): Boolean"))) + } + + @Test def importCompleteIncludeModule: Unit = { + withSources( + code"""object O { object MyObject }""", + code"""import O.My${m1}""" + ).completion(m1, Set(("MyObject", Module, "O.MyObject"))) + } + + @Test def importCompleteWithClassAndCompanion: Unit = { + withSources( + code"""package pkg0 + class Foo + object Foo""", + code"""package pgk1 + import pkg0.F${m1}""" + ).completion(m1, Set(("Foo", Class, "pkg0.Foo"))) + } + + @Test def importCompleteIncludePackage: Unit = { + withSources( + code"""package foo.bar + class Fizz""", + code"""import foo.b${m1}""" + ).completion(m1, Set(("bar", Module, "foo.bar"))) + } + + @Test def importCompleteIncludeMembers: Unit = { + withSources( + code"""object MyObject { + val myVal = 0 + def myDef = 0 + var myVar = 0 + object myObject + class myClass + trait myTrait + }""", + code"""import MyObject.my${m1}""" + ).completion(m1, Set(("myVal", Field, "Int"), + ("myDef", Method, "=> Int"), + ("myVar", Variable, "Int"), + ("myObject", Module, "MyObject.myObject"), + ("myClass", Class, "MyObject.myClass"), + ("myTrait", Class, "MyObject.myTrait"))) + } + + @Test def importJavaClass: Unit = { + code"""import java.io.FileDesc${m1}""".withSource + .completion(m1, Set(("FileDescriptor", Class, "java.io.FileDescriptor"))) + } + + @Test def importJavaStaticMethod: Unit = { + code"""import java.lang.System.lineSep${m1}""".withSource + .completion(m1, Set(("lineSeparator", Method, "(): String"))) + } + + @Test def importJavaStaticField: Unit = { + code"""import java.lang.System.ou${m1}""".withSource + .completion(m1, Set(("out", Field, "java.io.PrintStream"))) + } + + @Test def completeJavaModuleClass: Unit = { + code"""object O { + val out = java.io.FileDesc${m1} + }""".withSource + .completion(m1, Set(("FileDescriptor", Module, "java.io.FileDescriptor"))) + } + + @Test def importRename: Unit = { + code"""import java.io.{FileDesc${m1} => Foo}""".withSource + .completion(m1, Set(("FileDescriptor", Class, "java.io.FileDescriptor"))) + } + + @Test def markDeprecatedSymbols: Unit = { + code"""object Foo { + @deprecated + val bar = 0 + } + import Foo.ba${m1}""".withSource + .completion(m1, results => { + assertEquals(1, results.size) + val result = results.head + assertEquals("bar", result.getLabel) + assertTrue("bar was not deprecated", result.getDeprecated) + }) + } + + @Test def i4397: Unit = { + code"""class Foo { + | .${m1} + |}""".withSource + .completion(m1, Set()) + } + + @Test def completeNoPrefix: Unit = { + code"""class Foo { def foo = 0 } + |object Bar { + | val foo = new Foo + | foo.${m1} + |}""".withSource + .completion(m1, results => assertTrue(results.nonEmpty)) + } + + @Test def completeErrorKnowsKind: Unit = { + code"""object Bar { + | class Zig + | val Zag: Int = 0 + | val b = 3 + Bar.${m1} + |}""".withSource + .completion(m1, completionItems => { + val results = CodeCompletion.simplifyResults(completionItems) + assertTrue(results.contains(("Zag", Field, "Int"))) + assertFalse(results.exists((label, _, _) => label == "Zig")) + }) + } + + @Test def typeCompletionShowsTerm: Unit = { + code"""class Bar + |object Foo { + | val bar = new Bar + | def baz = new Bar + | object bat + | val bizz: ba${m1} + |}""".withSource + .completion(m1, Set(("bar", Field, "Bar"), ("bat", Module, "Foo.bat"))) } } diff --git a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala index 28036655b51e..53167188be9c 100644 --- a/language-server/test/dotty/tools/languageserver/util/CodeTester.scala +++ b/language-server/test/dotty/tools/languageserver/util/CodeTester.scala @@ -8,7 +8,9 @@ import dotty.tools.languageserver.util.server.{TestFile, TestServer} import dotty.tools.dotc.reporting.diagnostic.ErrorMessageID import dotty.tools.dotc.util.Signatures.Signature -import org.eclipse.lsp4j.{ CompletionItemKind, DocumentHighlightKind, Diagnostic, DiagnosticSeverity } +import org.eclipse.lsp4j.{ CompletionItem, CompletionItemKind, DocumentHighlightKind, Diagnostic, DiagnosticSeverity } + +import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals @@ -114,7 +116,19 @@ class CodeTester(projects: List[Project]) { * @see dotty.tools.languageserver.util.actions.CodeCompletion */ def completion(marker: CodeMarker, expected: Set[(String, CompletionItemKind, String)]): this.type = - doAction(new CodeCompletion(marker, expected)) + completion(marker, results => assertEquals(expected, CodeCompletion.simplifyResults(results))) + + /** + * Requests completion at the position defined by `marker`, and pass the results to + * `checkResults`. + * + * @param marker The position from which to ask for completions. + * @param checkResults A function that verifies that the results of completion are correct. + * + * @see dotty.tools.languageserver.util.actions.CodeCompletion + */ + def completion(marker: CodeMarker, checkResults: Set[CompletionItem] => Unit): this.type = + doAction(new CodeCompletion(marker, checkResults)) /** * Performs a workspace-wide renaming of the symbol under `marker`, verifies that the positions to diff --git a/language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala b/language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala index ff639039d505..90f7256c2293 100644 --- a/language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala +++ b/language-server/test/dotty/tools/languageserver/util/actions/CodeCompletion.scala @@ -4,7 +4,7 @@ import dotty.tools.languageserver.util.PositionContext import dotty.tools.languageserver.util.embedded.CodeMarker import dotty.tools.languageserver.util.server.TestFile -import org.eclipse.lsp4j.CompletionItemKind +import org.eclipse.lsp4j.{CompletionItem, CompletionItemKind} import org.junit.Assert.{assertEquals, assertFalse, assertTrue} import scala.collection.JavaConverters._ @@ -13,22 +13,28 @@ import scala.collection.JavaConverters._ * An action requesting for code completion at `marker`, expecting `expected`. * This action corresponds to the `textDocument/completion` method of the Language Server Protocol. * - * @param marker The marker indicating the position where completion should be requested. - * @param expected The expected results from the language server. + * @param marker The marker indicating the position where completion should be requested. + * @param checkResults A function that takes the results and verifies that they match + * expectations. */ class CodeCompletion(override val marker: CodeMarker, - expected: Set[(String, CompletionItemKind, String)]) extends ActionOnMarker { + checkResults: Set[CompletionItem] => Unit) + extends ActionOnMarker { override def execute(): Exec[Unit] = { val result = server.completion(marker.toCompletionParams).get() assertTrue(s"Completion results were not 'right': $result", result.isRight) assertFalse(s"Completion results were 'incomplete': $result", result.getRight.isIncomplete) - val completionResults = result.getRight.getItems.asScala.toSet.map { item => - (item.getLabel, item.getKind, item.getDetail) - } - assertEquals(expected, completionResults) + val completionResults = result.getRight.getItems.asScala.toSet + checkResults(completionResults) } override def show: PositionContext.PosCtx[String] = - s"CodeCompletion(${marker.show}, $expected)" + s"CodeCompletion(${marker.show}, $checkResults)" +} + +object CodeCompletion { + /** Extract the (label, kind, details) of each `CompletionItem`. */ + def simplifyResults(items: Set[CompletionItem]): Set[(String, CompletionItemKind, String)] = + items.map(item => (item.getLabel, item.getKind, item.getDetail)) }