diff --git a/compiler/src/dotty/tools/dotc/core/Flags.scala b/compiler/src/dotty/tools/dotc/core/Flags.scala index 5cff44ecfc4b..16bc965372f8 100644 --- a/compiler/src/dotty/tools/dotc/core/Flags.scala +++ b/compiler/src/dotty/tools/dotc/core/Flags.scala @@ -242,7 +242,7 @@ object Flags { val (AccessorOrSealed @ _, Accessor @ _, Sealed @ _) = newFlags(11, "", "sealed") /** A mutable var, an open class */ - val (MutableOrOpen @ __, Mutable @ _, Open @ _) = newFlags(12, "mutable", "open") + val (MutableOrOpen @ _, Mutable @ _, Open @ _) = newFlags(12, "mutable", "open") /** Symbol is local to current class (i.e. private[this] or protected[this] * pre: Private or Protected are also set diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index c1ddfe2b04e9..5237f19d19ae 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -1,6 +1,7 @@ package dotty.tools.dotc.interactive import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.ast.NavigateAST import dotty.tools.dotc.config.Printers.interactiv import dotty.tools.dotc.core.Contexts.* @@ -19,6 +20,8 @@ import dotty.tools.dotc.core.TypeError import dotty.tools.dotc.core.Phases import dotty.tools.dotc.core.Types.{AppliedType, ExprType, MethodOrPoly, NameFilter, NoType, RefinedType, TermRef, Type, TypeProxy} import dotty.tools.dotc.parsing.Tokens +import dotty.tools.dotc.typer.Implicits.SearchSuccess +import dotty.tools.dotc.typer.Inferencing import dotty.tools.dotc.util.Chars import dotty.tools.dotc.util.SourcePosition @@ -28,6 +31,7 @@ import dotty.tools.dotc.core.ContextOps.localContext import dotty.tools.dotc.core.Names import dotty.tools.dotc.core.Types import dotty.tools.dotc.core.Symbols +import dotty.tools.dotc.core.Constants /** * One of the results of a completion query. @@ -42,15 +46,35 @@ case class Completion(label: String, description: String, symbols: List[Symbol]) 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)(using Context): (Int, List[Completion]) = - val path: List[Tree] = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) - computeCompletions(pos, path)(using Interactive.contextOfPath(path).withPhase(Phases.typerPhase)) + val tpdPath = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) + val completionContext = Interactive.contextOfPath(tpdPath).withPhase(Phases.typerPhase) + inContext(completionContext): + val untpdPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos) + val mode = completionMode(untpdPath, pos) + val rawPrefix = completionPrefix(untpdPath, pos) + val completions = rawCompletions(pos, mode, rawPrefix, tpdPath, untpdPath) + + postProcessCompletions(untpdPath, completions, rawPrefix) + + /** Get possible completions from tree at `pos` + * This method requires manually computing the mode, prefix and paths. + * + * @return completion map of name to list of denotations + */ + def rawCompletions( + pos: SourcePosition, + mode: Mode, + rawPrefix: String, + tpdPath: List[tpd.Tree], + untpdPath: List[untpd.Tree] + )(using Context): CompletionMap = + val adjustedPath = typeCheckExtensionConstructPath(untpdPath, tpdPath, pos) + computeCompletions(pos, mode, rawPrefix, adjustedPath) /** * Inspect `path` to determine what kinds of symbols should be considered. @@ -63,20 +87,32 @@ object Completion: * Otherwise, provide no completion suggestion. */ def completionMode(path: List[untpd.Tree], pos: SourcePosition): Mode = - path match - case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport - case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport - case (ref: untpd.RefTree) :: _ => - if (ref.name.isTermName) Mode.Term - else if (ref.name.isTypeName) Mode.Type - else Mode.None - case (sel: untpd.ImportSelector) :: _ => - if sel.imported.span.contains(pos.span) then Mode.ImportOrExport - else Mode.None // Can't help completing the renaming + val completionSymbolKind: Mode = + path match + case untpd.Ident(_) :: untpd.Import(_, _) :: _ => Mode.ImportOrExport + case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.ImportOrExport + case untpd.Literal(Constants.Constant(_: String)) :: _ => Mode.Term // literal completions + case (ref: untpd.RefTree) :: _ => + if (ref.name.isTermName) Mode.Term + else if (ref.name.isTypeName) Mode.Type + else Mode.None + + case (sel: untpd.ImportSelector) :: _ => + if sel.imported.span.contains(pos.span) then Mode.ImportOrExport + else Mode.None // Can't help completing the renaming + + case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport + case _ => Mode.None + + val completionKind: Mode = + path match + case Nil | (_: untpd.PackageDef) :: _ => Mode.None + case untpd.Ident(_) :: (_: untpd.ImportSelector) :: _ => Mode.Member + case (_: untpd.Select) :: _ => Mode.Member + case _ => Mode.Scope - case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport - case _ => Mode.None + completionSymbolKind | completionKind /** When dealing with in varios palces we check to see if they are * due to incomplete backticks. If so, we ensure we get the full prefix @@ -99,12 +135,18 @@ object Completion: * returned prefix should be considered. */ def completionPrefix(path: List[untpd.Tree], pos: SourcePosition)(using Context): String = + def fallback: Int = + var i = pos.point - 1 + while i >= 0 && Chars.isIdentifierPart(pos.source.content()(i)) do i -= 1 + i + 1 + path match case (sel: untpd.ImportSelector) :: _ => completionPrefix(sel.imported :: Nil, pos) case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ if !sel.isGiven => - completionPrefix(sel.imported :: Nil, pos) + if sel.isWildcard then pos.source.content()(pos.point - 1).toString + else completionPrefix(sel.imported :: Nil, pos) case (tree: untpd.ImportOrExport) :: _ => tree.selectors.find(_.span.contains(pos.span)).map: selector => @@ -119,34 +161,19 @@ object Completion: case (ident: untpd.Ident) :: _ if ident.name == nme.ERROR => checkBacktickPrefix(ident.source.content(), ident.span.start, ident.span.end) - case (ref: untpd.RefTree) :: _ => - if (ref.name == nme.ERROR) "" - else ref.name.toString.take(pos.span.point - ref.span.point) + case (tree: untpd.RefTree) :: _ if tree.name != nme.ERROR => + tree.name.toString.take(pos.span.point - tree.span.point) + + case _ => pos.source.content.slice(fallback, pos.point).mkString - case _ => "" end completionPrefix /** Inspect `path` to determine the offset where the completion result should be inserted. */ def completionOffset(untpdPath: List[untpd.Tree]): Int = - untpdPath match { + untpdPath match case (ref: untpd.RefTree) :: _ => ref.span.point case _ => 0 - } - - /** Some information about the trees is lost after Typer such as Extension method construct - * is expanded into methods. In order to support completions in those cases - * we have to rely on untyped trees and only when types are necessary use typed trees. - */ - def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] = - lazy val untpdPath: List[untpd.Tree] = NavigateAST - .pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect: - case untpdTree: untpd.Tree => untpdTree - - tpdPath match - case (_: Bind) :: _ => tpdPath - case (_: untpd.TypTree) :: _ => tpdPath - case _ => untpdPath /** Handle case when cursor position is inside extension method construct. * The extension method construct is then desugared into methods, and consturct parameters @@ -159,8 +186,8 @@ object Completion: * @return Typed path to the parameter of the extension construct if found or tpdPath */ private def typeCheckExtensionConstructPath( - untpdPath: List[untpd.Tree], tpdPath: List[Tree], pos: SourcePosition - )(using Context): List[Tree] = + untpdPath: List[untpd.Tree], tpdPath: List[tpd.Tree], pos: SourcePosition + )(using Context): List[tpd.Tree] = untpdPath.collectFirst: case untpd.ExtMethods(paramss, _) => val enclosingParam = paramss.flatten.find(_.span.contains(pos.span)) @@ -170,38 +197,41 @@ object Completion: Interactive.pathTo(typedEnclosingParam, pos.span) .flatten.getOrElse(tpdPath) - private def computeCompletions(pos: SourcePosition, tpdPath: List[Tree])(using Context): (Int, List[Completion]) = - val path0 = resolveTypedOrUntypedPath(tpdPath, pos) - val mode = completionMode(path0, pos) - val rawPrefix = completionPrefix(path0, pos) - + private def computeCompletions( + pos: SourcePosition, mode: Mode, rawPrefix: String, adjustedPath: List[tpd.Tree] + )(using Context): CompletionMap = val hasBackTick = rawPrefix.headOption.contains('`') val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix - val completer = new Completer(mode, prefix, pos) - val adjustedPath = typeCheckExtensionConstructPath(path0, tpdPath, pos) - val completions = adjustedPath match - // Ignore synthetic select from `This` because in code it was `Ident` - // See example in dotty.tools.languageserver.CompletionTest.syntheticThis - case Select(qual @ This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions - case Select(qual, _) :: _ if qual.typeOpt.hasSimpleKind => completer.selectionCompletions(qual) - case Select(qual, _) :: _ => Map.empty - case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) - case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) - case _ => completer.scopeCompletions - + val result = adjustedPath match + // Ignore synthetic select from `This` because in code it was `Ident` + // See example in dotty.tools.languageserver.CompletionTest.syntheticThis + case tpd.Select(qual @ tpd.This(_), _) :: _ if qual.span.isSynthetic => completer.scopeCompletions + case tpd.Select(qual, _) :: _ if qual.typeOpt.hasSimpleKind => completer.selectionCompletions(qual) + case tpd.Select(qual, _) :: _ => Map.empty + case (tree: tpd.ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) + case (_: untpd.ImportSelector) :: tpd.Import(expr, _) :: _ => completer.directMemberCompletions(expr) + case _ => completer.scopeCompletions + + interactiv.println(i"""completion info with pos = $pos, + | prefix = ${completer.prefix}, + | term = ${completer.mode.is(Mode.Term)}, + | type = ${completer.mode.is(Mode.Type)}, + | scope = ${completer.mode.is(Mode.Scope)}, + | member = ${completer.mode.is(Mode.Member)}""") + + result + + def postProcessCompletions(path: List[untpd.Tree], completions: CompletionMap, rawPrefix: String)(using Context): (Int, List[Completion]) = val describedCompletions = describeCompletions(completions) + val hasBackTick = rawPrefix.headOption.contains('`') val backtickedCompletions = describedCompletions.map(completion => backtickCompletions(completion, hasBackTick)) - val offset = completionOffset(path0) + interactiv.println(i"""completion resutls = $backtickedCompletions%, %""") - interactiv.println(i"""completion with pos = $pos, - | prefix = ${completer.prefix}, - | term = ${completer.mode.is(Mode.Term)}, - | type = ${completer.mode.is(Mode.Type)} - | results = $backtickedCompletions%, %""") + val offset = completionOffset(path) (offset, backtickedCompletions) def backtickCompletions(completion: Completion, hasBackTick: Boolean) = @@ -249,6 +279,16 @@ object Completion: if denot.isType then denot.symbol.showFullName else denot.info.widenTermRefExpr.show + given ScopeOrdering(using Context): Ordering[Seq[SingleDenotation]] with + val order = + List(defn.ScalaPredefModuleClass, defn.ScalaPackageClass, defn.JavaLangPackageClass) + + override def compare(x: Seq[SingleDenotation], y: Seq[SingleDenotation]): Int = + val owner0 = x.headOption.map(_.symbol.effectiveOwner).getOrElse(NoSymbol) + val owner1 = y.headOption.map(_.symbol.effectiveOwner).getOrElse(NoSymbol) + + order.indexOf(owner0) - order.indexOf(owner1) + /** Computes code completions depending on the context in which completion is requested * @param mode Should complete names of terms, types or both * @param prefix The prefix that all suggested completions should start with @@ -265,13 +305,22 @@ object Completion: * This mimics the logic for deciding what is ambiguous used by the compiler. * In general in case of a name clash symbols introduced in more deeply nested scopes * have higher priority and shadow previous definitions with the same name although: - * - imports with the same level of nesting cause an ambiguity + * - imports with the same level of nesting cause an ambiguity if they are in the same name space * - members and local definitions with the same level of nesting are allowed for overloading * - an import is ignored if there is a local definition or a member introduced in the same scope * (even if the import follows it syntactically) * - a more deeply nested import shadowing a member or a local definition causes an ambiguity */ def scopeCompletions(using context: Context): CompletionMap = + + /** Temporary data structure representing denotations with the same name introduced in a given scope + * as a member of a type, by a local definition or by an import clause + */ + case class ScopedDenotations private (denots: Seq[SingleDenotation], ctx: Context) + object ScopedDenotations: + def apply(denots: Seq[SingleDenotation], ctx: Context, includeFn: SingleDenotation => Boolean): ScopedDenotations = + ScopedDenotations(denots.filter(includeFn), ctx) + val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty) def addMapping(name: Name, denots: ScopedDenotations) = mappings(name) = mappings(name) :+ denots @@ -279,18 +328,18 @@ object Completion: ctx.outersIterator.foreach { case ctx @ given Context => if ctx.isImportContext then importedCompletions.foreach { (name, denots) => - addMapping(name, ScopedDenotations(denots, ctx)) + addMapping(name, ScopedDenotations(denots, ctx, include(_, name))) } else if ctx.owner.isClass then accessibleMembers(ctx.owner.thisType) .groupByName.foreach { (name, denots) => - addMapping(name, ScopedDenotations(denots, ctx)) + addMapping(name, ScopedDenotations(denots, ctx, include(_, name))) } else if ctx.scope ne EmptyScope then ctx.scope.toList.filter(symbol => include(symbol, symbol.name)) .flatMap(_.alternatives) .groupByName.foreach { (name, denots) => - addMapping(name, ScopedDenotations(denots, ctx)) + addMapping(name, ScopedDenotations(denots, ctx, include(_, name))) } } @@ -303,26 +352,22 @@ object Completion: def isSingleImport = denotss.length < 2 // import a.C // locally { import b.C } - def isImportedInDifferentScope = first.ctx.scope ne denotss(1).ctx.scope + def isImportedInDifferentScope = first.ctx.scope ne denotss(1).ctx.scope // import a.C // import a.C - def isSameSymbolImportedDouble = denotss.forall(_.denots == first.denots) - - def isScalaPackage(scopedDenots: ScopedDenotations) = - scopedDenots.denots.exists(_.info.typeSymbol.owner == defn.ScalaPackageClass) - - def isJavaLangPackage(scopedDenots: ScopedDenotations) = - scopedDenots.denots.exists(_.info.typeSymbol.owner == defn.JavaLangPackageClass) - - // For example - // import java.lang.annotation - // is shadowed by - // import scala.annotation - def isJavaLangAndScala = - try - denotss.forall(denots => isScalaPackage(denots) || isJavaLangPackage(denots)) - catch - case NonFatal(_) => false + def isSameSymbolImportedDouble = denotss.forall(_.denots == first.denots) + + // https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html + // import java.lang.* + // { + // import scala.* + // { + // import Predef.* + // { /* source */ } + // } + // } + def notConflictingWithDefaults = // is imported symbol + denotss.filterNot(_.denots.exists(denot => Interactive.isImportedByDefault(denot.symbol))).size <= 1 denotss.find(!_.ctx.isImportContext) match { // most deeply nested member or local definition if not shadowed by an import @@ -331,13 +376,9 @@ object Completion: case None if isSingleImport || isImportedInDifferentScope || isSameSymbolImportedDouble => resultMappings += name -> first.denots - case None if isJavaLangAndScala => - denotss.foreach{ - denots => - if isScalaPackage(denots) then - resultMappings += name -> denots.denots - } - + case None if notConflictingWithDefaults => + val ordered = denotss.map(_.denots).sorted + resultMappings += name -> ordered.head case _ => } } @@ -347,7 +388,7 @@ object Completion: /** Widen only those types which are applied or are exactly nothing */ - def widenQualifier(qual: Tree)(using Context): Tree = + def widenQualifier(qual: tpd.Tree)(using Context): tpd.Tree = qual.typeOpt.widenDealias match case widenedType if widenedType.isExactlyNothing => qual.withType(widenedType) case appliedType: AppliedType => qual.withType(appliedType) @@ -357,7 +398,7 @@ object Completion: * Direct members take priority over members from extensions * and so do members from extensions over members from implicit conversions */ - def selectionCompletions(qual: Tree)(using Context): CompletionMap = + def selectionCompletions(qual: tpd.Tree)(using Context): CompletionMap = val adjustedQual = widenQualifier(qual) implicitConversionMemberCompletions(adjustedQual) ++ @@ -367,7 +408,7 @@ object Completion: /** Completions for members of `qual`'s type. * These include inherited definitions but not members added by extensions or implicit conversions */ - def directMemberCompletions(qual: Tree)(using Context): CompletionMap = + def directMemberCompletions(qual: tpd.Tree)(using Context): CompletionMap = if qual.typeOpt.isExactlyNothing then Map.empty else @@ -414,17 +455,28 @@ object Completion: end importedCompletions /** Completions from implicit conversions including old style extensions using implicit classes */ - private def implicitConversionMemberCompletions(qual: Tree)(using Context): CompletionMap = + private def implicitConversionMemberCompletions(qual: tpd.Tree)(using Context): CompletionMap = + + def tryToInstantiateTypeVars(conversionTarget: SearchSuccess): Type = + try + val typingCtx = ctx.fresh + inContext(typingCtx): + val methodRefTree = tpd.ref(conversionTarget.ref, needLoad = false) + val convertedTree = ctx.typer.typedAheadExpr(untpd.Apply(untpd.TypedSplice(methodRefTree), untpd.TypedSplice(qual) :: Nil)) + Inferencing.fullyDefinedType(convertedTree.tpe, "", pos) + catch + case error => conversionTarget.tree.tpe // fallback to not fully defined type + if qual.typeOpt.isExactlyNothing || qual.typeOpt.isNullType then Map.empty else implicitConversionTargets(qual)(using ctx.fresh.setExploreTyperState()) - .flatMap(accessibleMembers) + .flatMap { conversionTarget => accessibleMembers(tryToInstantiateTypeVars(conversionTarget)) } .toSeq .groupByName /** Completions from extension methods */ - private def extensionCompletions(qual: Tree)(using Context): CompletionMap = + private def extensionCompletions(qual: tpd.Tree)(using Context): CompletionMap = def asDefLikeType(tpe: Type): Type = tpe match case _: MethodOrPoly => tpe case _ => ExprType(tpe) @@ -551,19 +603,14 @@ object Completion: * @param qual The argument to which the implicit conversion should be applied. * @return The set of types after `qual` implicit conversion. */ - private def implicitConversionTargets(qual: Tree)(using Context): Set[Type] = + private def implicitConversionTargets(qual: tpd.Tree)(using Context): Set[SearchSuccess] = { val typer = ctx.typer - val targets = try { - val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span).allImplicits - conversions.map(_.tree.typeOpt) - } catch { - case _ => - interactiv.println(i"implicit conversion targets failed: ${qual.show}") - Set.empty - } + val conversions = new typer.ImplicitSearch(defn.AnyType, qual, pos.span).allImplicits + conversions.map(_.tree.typeOpt) - interactiv.println(i"implicit conversion targets considered: ${targets.toList}%, %") - targets + interactiv.println(i"implicit conversion targets considered: ${conversions.toList}%, %") + conversions + } /** Filter for names that should appear when looking for completions. */ private object completionsFilter extends NameFilter: @@ -580,11 +627,6 @@ object Completion: private type CompletionMap = Map[Name, Seq[SingleDenotation]] - /** Temporary data structure representing denotations with the same name introduced in a given scope - * as a member of a type, by a local definition or by an import clause - */ - private case class ScopedDenotations(denots: Seq[SingleDenotation], ctx: Context) - /** * The completion mode: defines what kinds of symbols should be included in the completion * results. @@ -606,3 +648,7 @@ object Completion: /** Both term and type symbols are allowed */ val ImportOrExport: Mode = new Mode(4) | Term | Type + val Scope: Mode = new Mode(8) + + val Member: Mode = new Mode(16) + diff --git a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala index c181c340d66d..928a9be6103b 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Interactive.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Interactive.scala @@ -420,6 +420,21 @@ object Interactive { false } + + /** Some information about the trees is lost after Typer such as Extension method construct + * is expanded into methods. In order to support completions in those cases + * we have to rely on untyped trees and only when types are necessary use typed trees. + */ + def resolveTypedOrUntypedPath(tpdPath: List[Tree], pos: SourcePosition)(using Context): List[untpd.Tree] = + lazy val untpdPath: List[untpd.Tree] = NavigateAST + .pathTo(pos.span, List(ctx.compilationUnit.untpdTree), true).collect: + case untpdTree: untpd.Tree => untpdTree + + tpdPath match + case (_: Bind) :: _ => tpdPath + case (_: untpd.TypTree) :: _ => tpdPath + case _ => untpdPath + /** * Is this tree using a renaming introduced by an import statement or an alias for `this`? * @@ -436,6 +451,20 @@ object Interactive { def sameName(n0: Name, n1: Name): Boolean = n0.stripModuleClassSuffix.toTermName eq n1.stripModuleClassSuffix.toTermName + /** https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html + * import java.lang.* + * { + * import scala.* + * { + * import Predef.* + * { /* source */ } + * } + * } + */ + def isImportedByDefault(sym: Symbol)(using Context): Boolean = + val owner = sym.effectiveOwner + owner == defn.ScalaPredefModuleClass || owner == defn.ScalaPackageClass || owner == defn.JavaLangPackageClass + private[interactive] def safely[T](op: => List[T]): List[T] = try op catch { case ex: TypeError => Nil } } diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 7a21fe15fbe5..335516d8c7f9 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1019,11 +1019,11 @@ class CompletionTest { | val x = Bar.${m1}""" .completion( ("getClass", Method, "[X0 >: Foo.Bar.type](): Class[? <: X0]"), - ("ensuring", Method, "(cond: Boolean): A"), + ("ensuring", Method, "(cond: Boolean): Foo.Bar.type"), ("##", Method, "=> Int"), ("nn", Method, "=> Foo.Bar.type"), ("==", Method, "(x$0: Any): Boolean"), - ("ensuring", Method, "(cond: Boolean, msg: => Any): A"), + ("ensuring", Method, "(cond: Boolean, msg: => Any): Foo.Bar.type"), ("ne", Method, "(x$0: Object): Boolean"), ("valueOf", Method, "($name: String): Foo.Bar"), ("equals", Method, "(x$0: Any): Boolean"), @@ -1031,21 +1031,21 @@ class CompletionTest { ("hashCode", Method, "(): Int"), ("notifyAll", Method, "(): Unit"), ("values", Method, "=> Array[Foo.Bar]"), - ("→", Method, "[B](y: B): (A, B)"), + ("→", Method, "[B](y: B): (Foo.Bar.type, B)"), ("!=", Method, "(x$0: Any): Boolean"), ("fromOrdinal", Method, "(ordinal: Int): Foo.Bar"), ("asInstanceOf", Method, "[X0]: X0"), - ("->", Method, "[B](y: B): (A, B)"), + ("->", Method, "[B](y: B): (Foo.Bar.type, B)"), ("wait", Method, "(x$0: Long, x$1: Int): Unit"), ("`back-tick`", Field, "Foo.Bar"), ("notify", Method, "(): Unit"), ("formatted", Method, "(fmtstr: String): String"), - ("ensuring", Method, "(cond: A => Boolean, msg: => Any): A"), + ("ensuring", Method, "(cond: Foo.Bar.type => Boolean, msg: => Any): Foo.Bar.type"), ("wait", Method, "(): Unit"), ("isInstanceOf", Method, "[X0]: Boolean"), ("`match`", Field, "Foo.Bar"), ("toString", Method, "(): String"), - ("ensuring", Method, "(cond: A => Boolean): A"), + ("ensuring", Method, "(cond: Foo.Bar.type => Boolean): Foo.Bar.type"), ("eq", Method, "(x$0: Object): Boolean"), ("synchronized", Method, "[X0](x$0: X0): X0") ) @@ -1576,6 +1576,61 @@ class CompletionTest { |""" .completion(m1, Set(("TTT", Field, "T.TTT"))) + @Test def properTypeVariable: Unit = + code"""|object M: + | List(1,2,3).filterNo$m1 + |""" + .completion(m1, Set(("filterNot", Method, "(p: Int => Boolean): List[Int]"))) + + @Test def properTypeVariableForExtensionMethods: Unit = + code"""|object M: + | extension [T](x: List[T]) def test(aaa: T): T = ??? + | List(1,2,3).tes$m1 + | + |""" + .completion(m1, Set(("test", Method, "(aaa: Int): Int"))) + + @Test def properTypeVariableForExtensionMethodsByName: Unit = + code"""|object M: + | extension [T](xs: List[T]) def test(p: T => Boolean): List[T] = ??? + | List(1,2,3).tes$m1 + |""" + .completion(m1, Set(("test", Method, "(p: Int => Boolean): List[Int]"))) + + @Test def genericExtensionTypeParameterInference: Unit = + code"""|object M: + | extension [T](xs: T) def test(p: T): T = ??? + | 3.tes$m1 + |""" + .completion(m1, Set(("test", Method, "(p: Int): Int"))) + + @Test def genericExtensionTypeParameterInferenceByName: Unit = + code"""|object M: + | extension [T](xs: T) def test(p: T => Boolean): T = ??? + | 3.tes$m1 + |""" + .completion(m1, Set(("test", Method, "(p: Int => Boolean): Int"))) + + @Test def properTypeVariableForImplicitDefs: Unit = + code"""|object M: + | implicit class ListUtils[T](xs: List[T]) { + | def test(p: T => Boolean): List[T] = ??? + | } + | List(1,2,3).tes$m1 + |""" + .completion(m1, Set(("test", Method, "(p: Int => Boolean): List[Int]"))) + + @Test def properTypeParameterForImplicitDefs: Unit = + code"""|object M: + | implicit class ListUtils[T](xs: T) { + | def test(p: T => Boolean): T = ??? + | } + | new ListUtils(1).tes$m1 + | 1.tes$m2 + |""" + .completion(m1, Set(("test", Method, "(p: Int => Boolean): Int"))) + .completion(m2, Set(("test", Method, "(p: Int => Boolean): Int"))) + @Test def selectDynamic: Unit = code"""|import scala.language.dynamics |class Foo extends Dynamic { @@ -1591,4 +1646,37 @@ class CompletionTest { |""" .completion(m1, Set(("selectDynamic", Method, "(field: String): Foo"))) .completion(m2, Set(("banana", Method, "=> Int"))) + + @Test def shadowedImport: Unit = + code"""| + |import Matches.* + |object Matches { + | val Number = "".r + |} + |object Main { + | Num$m1 + |} + |""".completion(m1, Set( + ("Number", Field, "scala.util.matching.Regex"), + ("NumberFormatException", Module, "NumberFormatException"), + ("Numeric", Method, "=> scala.math.Numeric.type") + )) + + @Test def shadowedImportType: Unit = + code"""| + |import Matches.* + |object Matches { + | val Number = "".r + |} + |object Main { + | val x: Num$m1 + |} + |""".completion(m1, Set( + ("Number", Class, "Number"), + ("Number", Field, "scala.util.matching.Regex"), + ("NumberFormatException", Module, "NumberFormatException"), + ("NumberFormatException", Field, "NumberFormatException"), + ("Numeric", Field, "Numeric"), + ("Numeric", Method, "=> scala.math.Numeric.type") + )) } diff --git a/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala b/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala index e31f4756b220..ad6fe9420a81 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/IndexedContext.scala @@ -10,6 +10,7 @@ import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.Scopes.EmptyScope import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* +import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.typer.ImportInfo import dotty.tools.pc.IndexedContext.Result import dotty.tools.pc.utils.MtagsEnrichments.* @@ -31,16 +32,33 @@ sealed trait IndexedContext: case Some(symbols) if symbols.exists(_ == sym) => Result.InScope case Some(symbols) - if symbols - .exists(s => isTypeAliasOf(s, sym) || isTermAliasOf(s, sym)) => - Result.InScope + if symbols.exists(s => isNotConflictingWithDefault(s, sym) || isTypeAliasOf(s, sym) || isTermAliasOf(s, sym)) => + Result.InScope // when all the conflicting symbols came from an old version of the file - case Some(symbols) if symbols.nonEmpty && symbols.forall(_.isStale) => - Result.Missing + case Some(symbols) if symbols.nonEmpty && symbols.forall(_.isStale) => Result.Missing case Some(_) => Result.Conflict case None => Result.Missing end lookupSym + /** + * Scala by default imports following packages: + * https://scala-lang.org/files/archive/spec/3.4/02-identifiers-names-and-scopes.html + * import java.lang.* + * { + * import scala.* + * { + * import Predef.* + * { /* source */ } + * } + * } + * + * This check is necessary for proper scope resolution, because when we compare symbols from + * index including the underlying type like scala.collection.immutable.List it actually + * is in current scope in form of type forwarder imported from Predef. + */ + private def isNotConflictingWithDefault(sym: Symbol, queriedSym: Symbol): Boolean = + sym.info.widenDealias =:= queriedSym.info.widenDealias && (Interactive.isImportedByDefault(sym)) + final def hasRename(sym: Symbol, as: String): Boolean = rename(sym) match case Some(v) => v == as @@ -49,15 +67,15 @@ sealed trait IndexedContext: // detects import scope aliases like // object Predef: // val Nil = scala.collection.immutable.Nil - private def isTermAliasOf(termAlias: Symbol, sym: Symbol): Boolean = + private def isTermAliasOf(termAlias: Symbol, queriedSym: Symbol): Boolean = termAlias.isTerm && ( - sym.info match + queriedSym.info match case clz: ClassInfo => clz.appliedRef =:= termAlias.info.resultType case _ => false ) - private def isTypeAliasOf(alias: Symbol, sym: Symbol): Boolean = - alias.isAliasType && alias.info.metalsDealias.typeSymbol == sym + private def isTypeAliasOf(alias: Symbol, queriedSym: Symbol): Boolean = + alias.isAliasType && alias.info.metalsDealias.typeSymbol == queriedSym final def isEmpty: Boolean = this match case IndexedContext.Empty => true diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala index 415bef2cff25..53d867c924a6 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionPos.scala @@ -5,23 +5,20 @@ import java.net.URI import scala.meta.pc.OffsetParams -import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.ast.untpd.* import dotty.tools.dotc.ast.untpd.ImportSelector import dotty.tools.dotc.core.Contexts.* import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.util.Chars import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.util.Spans +import dotty.tools.dotc.interactive.Completion import dotty.tools.pc.utils.MtagsEnrichments.* import org.eclipse.lsp4j as l import scala.annotation.tailrec -enum CompletionKind: - case Empty, Scope, Members - case class CompletionPos( - kind: CompletionKind, start: Int, end: Int, query: String, @@ -40,29 +37,21 @@ object CompletionPos: def infer( cursorPos: SourcePosition, offsetParams: OffsetParams, - treePath: List[Tree] + adjustedPath: List[Tree] )(using Context): CompletionPos = - infer(cursorPos, offsetParams.uri().nn, offsetParams.text().nn, treePath) + infer(cursorPos, offsetParams.uri().nn, offsetParams.text().nn, adjustedPath) def infer( cursorPos: SourcePosition, uri: URI, text: String, - treePath: List[Tree] + adjustedPath: List[Tree] )(using Context): CompletionPos = - val start = inferIdentStart(cursorPos, text, treePath) - val end = inferIdentEnd(cursorPos, text) - val query = text.substring(start, end) - val prevIsDot = - if start - 1 >= 0 then text.charAt(start - 1) == '.' else false - val kind = - if prevIsDot then CompletionKind.Members - else if isImportOrExportSelect(cursorPos, treePath) then - CompletionKind.Members - else if query.nn.isEmpty then CompletionKind.Empty - else CompletionKind.Scope + val identEnd = inferIdentEnd(cursorPos, text) + val query = Completion.completionPrefix(adjustedPath, cursorPos) + val start = cursorPos.point - query.length() - CompletionPos(kind, start, end, query.nn, cursorPos, uri) + CompletionPos(start, identEnd, query.nn, cursorPos, uri) end infer /** @@ -87,57 +76,6 @@ object CompletionPos: (i, tabIndented) end inferIndent - private def isImportOrExportSelect( - pos: SourcePosition, - path: List[Tree], - )(using Context): Boolean = - @tailrec - def loop(enclosing: List[Tree]): Boolean = - enclosing match - case head :: tl if !head.sourcePos.contains(pos) => loop(tl) - case (tree: (Import | Export)) :: _ => - tree.selectors.exists(_.imported.sourcePos.contains(pos)) - case _ => false - - loop(path) - - - /** - * Returns the start offset of the identifier starting as the given offset position. - */ - private def inferIdentStart( - pos: SourcePosition, - text: String, - path: List[Tree] - )(using Context): Int = - def fallback: Int = - var i = pos.point - 1 - while i >= 0 && Chars.isIdentifierPart(text.charAt(i)) do i -= 1 - i + 1 - def loop(enclosing: List[Tree]): Int = - enclosing match - case Nil => fallback - case head :: tl => - if !head.sourcePos.contains(pos) then loop(tl) - else - head match - case i: Ident => i.sourcePos.point - case s: Select => - if s.name.toTermName == nme.ERROR || s.span.exists && pos.span.point < s.span.point - then fallback - else s.span.point - case Import(_, sel) => - sel - .collectFirst { - case ImportSelector(imported, renamed, _) - if imported.sourcePos.contains(pos) => - imported.sourcePos.point - } - .getOrElse(fallback) - case _ => fallback - loop(path) - end inferIdentStart - /** * Returns the end offset of the identifier starting as the given offset position. */ diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala index b3b7d1fde07d..302219e12e10 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionProvider.scala @@ -13,6 +13,7 @@ import dotty.tools.dotc.ast.tpd import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Phases import dotty.tools.dotc.core.StdNames import dotty.tools.dotc.interactive.Interactive import dotty.tools.dotc.interactive.InteractiveDriver @@ -53,14 +54,16 @@ class CompletionProvider( val (items, isIncomplete) = driver.compilationUnits.get(uri) match case Some(unit) => - val newctx = ctx.fresh.setCompilationUnit(unit) + val newctx = ctx.fresh.setCompilationUnit(unit).withPhase(Phases.typerPhase(using ctx)) val tpdPath = Interactive.pathTo(newctx.compilationUnit.tpdTree, pos.span)(using newctx) + val adjustedPath = Interactive.resolveTypedOrUntypedPath(tpdPath, pos)(using newctx) - val locatedCtx = - Interactive.contextOfPath(tpdPath)(using newctx) + val locatedCtx = Interactive.contextOfPath(tpdPath)(using newctx) val indexedCtx = IndexedContext(locatedCtx) + val completionPos = - CompletionPos.infer(pos, params, tpdPath)(using newctx) + CompletionPos.infer(pos, params, adjustedPath)(using locatedCtx) + val autoImportsGen = AutoImports.generator( completionPos.sourcePos, text, @@ -69,16 +72,18 @@ class CompletionProvider( indexedCtx, config ) + val (completions, searchResult) = new Completions( pos, text, - ctx.fresh.setCompilationUnit(unit), + locatedCtx, search, buildTargetIdentifier, completionPos, indexedCtx, tpdPath, + adjustedPath, config, folderPath, autoImportsGen, @@ -94,7 +99,7 @@ class CompletionProvider( completionPos, tpdPath, indexedCtx - )(using newctx) + )(using locatedCtx) } val isIncomplete = searchResult match case SymbolSearch.Result.COMPLETE => false diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala index a890afa3cb67..f6217ba21ebf 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/CompletionValue.scala @@ -4,6 +4,7 @@ package completions import scala.meta.internal.pc.CompletionItemData import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Denotations.Denotation import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.Symbols.Symbol import dotty.tools.dotc.core.Types.Type @@ -33,6 +34,7 @@ enum CompletionSource: case MatchCompletionKind case CaseKeywordKind case DocumentKind + case ImplicitClassKind sealed trait CompletionValue: def label: String @@ -61,7 +63,8 @@ end CompletionValue object CompletionValue: sealed trait Symbolic extends CompletionValue: - def symbol: Symbol + def denotation: Denotation + val symbol = denotation.symbol def isFromWorkspace: Boolean = false override def completionItemDataKind = CompletionItemData.None def isExtensionMethod: Boolean = false @@ -114,29 +117,28 @@ object CompletionValue: s"${label}${printedParams.mkString("[", ",", "]")}" else label - override def description(printer: ShortenedTypePrinter)( - using Context - ): String = - printer.completionSymbol(symbol) + override def description(printer: ShortenedTypePrinter)(using Context): String = + printer.completionSymbol(denotation) + end Symbolic case class Compiler( label: String, - symbol: Symbol, + denotation: Denotation, override val snippetSuffix: CompletionSuffix - ) extends Symbolic { + ) extends Symbolic: override def completionItemDataKind: Integer = CompletionSource.CompilerKind.ordinal - } + case class Scope( label: String, - symbol: Symbol, + denotation: Denotation, override val snippetSuffix: CompletionSuffix, - ) extends Symbolic { + ) extends Symbolic: override def completionItemDataKind: Integer = CompletionSource.ScopeKind.ordinal - } + case class Workspace( label: String, - symbol: Symbol, + denotation: Denotation, override val snippetSuffix: CompletionSuffix, override val importSymbol: Symbol ) extends Symbolic: @@ -148,21 +150,22 @@ object CompletionValue: */ case class ImplicitClass( label: String, - symbol: Symbol, + denotation: Denotation, override val snippetSuffix: CompletionSuffix, override val importSymbol: Symbol, ) extends Symbolic: override def completionItemKind(using Context): CompletionItemKind = CompletionItemKind.Method + override def completionItemDataKind: Integer = CompletionSource.ImplicitClassKind.ordinal override def description(printer: ShortenedTypePrinter)(using Context): String = - s"${printer.completionSymbol(symbol)} (implicit)" + s"${printer.completionSymbol(denotation)} (implicit)" /** * CompletionValue for extension methods via SymbolSearch */ case class Extension( label: String, - symbol: Symbol, + denotation: Denotation, override val snippetSuffix: CompletionSuffix ) extends Symbolic: override def completionItemKind(using Context): CompletionItemKind = @@ -170,7 +173,7 @@ object CompletionValue: override def completionItemDataKind: Integer = CompletionSource.ExtensionKind.ordinal override def isExtensionMethod: Boolean = true override def description(printer: ShortenedTypePrinter)(using Context): String = - s"${printer.completionSymbol(symbol)} (extension)" + s"${printer.completionSymbol(denotation)} (extension)" /** * @param shortenedNames shortened type names by `Printer`. This field should be used for autoImports @@ -183,7 +186,7 @@ object CompletionValue: case class Override( label: String, value: String, - symbol: Symbol, + denotation: Denotation, override val additionalEdits: List[TextEdit], override val filterText: Option[String], override val range: Option[Range] @@ -199,7 +202,7 @@ object CompletionValue: case class NamedArg( label: String, tpe: Type, - symbol: Symbol + denotation: Denotation ) extends Symbolic: override def insertText: Option[String] = Some(label.replace("$", "$$").nn) override def completionItemDataKind: Integer = CompletionSource.OverrideKind.ordinal @@ -249,7 +252,7 @@ object CompletionValue: CompletionItemKind.Folder case class Interpolator( - symbol: Symbol, + denotation: Denotation, label: String, override val insertText: Option[String], override val additionalEdits: List[TextEdit], @@ -280,7 +283,7 @@ object CompletionValue: desc case class CaseKeyword( - symbol: Symbol, + denotation: Denotation, label: String, override val insertText: Option[String], override val additionalEdits: List[TextEdit], diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala index aead7b754fa6..68b7f51a40c6 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/Completions.scala @@ -12,6 +12,8 @@ import scala.meta.internal.pc.{IdentifierComparator, MemberOrdering} import scala.meta.pc.* import dotty.tools.dotc.ast.tpd.* +import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.ast.NavigateAST import dotty.tools.dotc.core.Comments.Comment import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.* @@ -31,6 +33,8 @@ import dotty.tools.pc.AutoImports.AutoImportsGenerator import dotty.tools.pc.completions.OverrideCompletions.OverrideExtractor import dotty.tools.pc.buildinfo.BuildInfo import dotty.tools.pc.utils.MtagsEnrichments.* +import dotty.tools.dotc.core.Denotations.SingleDenotation +import dotty.tools.dotc.interactive.Interactive class Completions( pos: SourcePosition, @@ -41,6 +45,7 @@ class Completions( completionPos: CompletionPos, indexedContext: IndexedContext, path: List[Tree], + adjustedPath: List[untpd.Tree], config: PresentationCompilerConfig, workspace: Option[Path], autoImports: AutoImportsGenerator, @@ -48,16 +53,10 @@ class Completions( options: List[String] )(using ReportContext): - implicit val context: Context = ctx + given context: Context = ctx - val coursierComplete = new CoursierComplete(BuildInfo.scalaVersion) - - private lazy val adjustedPath = Completion.resolveTypedOrUntypedPath(path, pos) - private lazy val completionMode = - val mode = Completion.completionMode(adjustedPath, pos) - path match - case Literal(Constant(_: String)) :: _ => Mode.Term // literal completions - case _ => mode + private lazy val coursierComplete = new CoursierComplete(BuildInfo.scalaVersion) + private lazy val completionMode = Completion.completionMode(adjustedPath, pos) private lazy val shouldAddSnippet = path match @@ -122,14 +121,16 @@ class Completions( case Select(qual, _) :: _ if qual.typeOpt.isErroneous => (allAdvanced, SymbolSearch.Result.COMPLETE) case Select(qual, _) :: _ => - val (_, compilerCompletions) = Completion.completions(pos) + val compilerCompletions = Completion.rawCompletions(pos, completionMode, completionPos.query, path, adjustedPath) val (compiler, result) = compilerCompletions + .toList .flatMap(toCompletionValues) .filterInteresting(qual.typeOpt.widenDealias) (allAdvanced ++ compiler, result) case _ => - val (_, compilerCompletions) = Completion.completions(pos) + val compilerCompletions = Completion.rawCompletions(pos, completionMode, completionPos.query, path, adjustedPath) val (compiler, result) = compilerCompletions + .toList .flatMap(toCompletionValues) .filterInteresting() (allAdvanced ++ compiler, result) @@ -143,15 +144,15 @@ class Completions( end completions private def toCompletionValues( - completion: Completion + completion: Name, + denots: Seq[SingleDenotation] ): List[CompletionValue] = - completion.symbols.flatMap( + denots.toList.flatMap: denot => completionsWithSuffix( - _, - completion.label, - CompletionValue.Compiler(_, _, _) + denot, + completion.show, + (label, denot, suffix) => CompletionValue.Compiler(label, denot, suffix) ) - ) end toCompletionValues inline private def undoBacktick(label: String): String = @@ -226,35 +227,35 @@ class Completions( end findSuffix def completionsWithSuffix( - sym: Symbol, + denot: SingleDenotation, label: String, - toCompletionValue: (String, Symbol, CompletionSuffix) => CompletionValue + toCompletionValue: (String, SingleDenotation, CompletionSuffix) => CompletionValue ): List[CompletionValue] = - // workaround for earlier versions that force correctly detecting Java flags - - def companionSynthetic = sym.companion.exists && sym.companion.is(Synthetic) + val sym = denot.symbol // find the apply completion that would need a snippet - val methodSymbols = + // handling is LTS specific: It's a workaround to mittigate lack of https://github.com/scala/scala3/pull/18874 backport + // which was required in backport of https://github.com/scala/scala3/pull/18914 to achive the improved completions semantics. + val methodDenots: List[SingleDenotation] = if shouldAddSnippet && completionMode.is(Mode.Term) && - (sym.is(Flags.Module) || sym.isClass && !sym.is(Flags.Trait)) && !sym.is(Flags.JavaDefined) + (sym.isOneOf(Flags.Module | Flags.Accessor) || sym.isField || sym.isClass && !sym.is(Flags.Trait)) && !sym.is(Flags.JavaDefined) then val info = /* Companion will be added even for normal classes now, * but it will not show up from classpath. We can suggest * constructors based on those synthetic applies. */ - if sym.isClass && companionSynthetic then sym.companionModule.info - else sym.info - val applSymbols = info.member(nme.apply).allSymbols - sym :: applSymbols - else List(sym) - - methodSymbols.map { methodSymbol => - val suffix = findSuffix(methodSymbol) + if sym.isClass && sym.companionModule.exists then sym.companionModule.info + else denot.info + val applyDenots = info.member(nme.apply).allSymbols.map(_.asSingleDenotation) + denot :: applyDenots + else denot :: Nil + + methodDenots.map { methodDenot => + val suffix = findSuffix(methodDenot.symbol) val name = undoBacktick(label) toCompletionValue( name, - methodSymbol, + methodDenot, suffix ) } @@ -496,68 +497,64 @@ class Completions( qualType: Type = ctx.definitions.AnyType ): Option[SymbolSearch.Result] = val query = completionPos.query - completionPos.kind match - case CompletionKind.Empty => - val filtered = indexedContext.scopeSymbols - .filter(sym => - !sym.isConstructor && (!sym.is(Synthetic) || sym.is(Module)) - ) - - filtered.map { sym => - visit(CompletionValue.Scope(sym.decodedName, sym, findSuffix(sym))) - } - Some(SymbolSearch.Result.INCOMPLETE) - case CompletionKind.Scope => - val visitor = new CompilerSearchVisitor(sym => - indexedContext.lookupSym(sym) match - case IndexedContext.Result.InScope => - visit(CompletionValue.Scope(sym.decodedName, sym, findSuffix(sym))) - case _ => - completionsWithSuffix( - sym, - sym.decodedName, - CompletionValue.Workspace(_, _, _, sym) - ).map(visit).forall(_ == true), - ) - Some(search.search(query, buildTargetIdentifier, visitor).nn) - case CompletionKind.Members => - val visitor = new CompilerSearchVisitor(sym => - def isExtensionMethod = sym.is(ExtensionMethod) && - qualType.widenDealias <:< sym.extensionParam.info.widenDealias - def isImplicitClass(owner: Symbol) = - val constructorParam = - owner.info - .membersBasedOnFlags( - Flags.ParamAccessor, - Flags.EmptyFlags, - ) - .headOption - .map(_.info) - owner.isClass && owner.is(Flags.Implicit) && - constructorParam.exists(p => - qualType.widenDealias <:< p.widenDealias - ) - end isImplicitClass - - def isImplicitClassMethod = sym.is(Flags.Method) && !sym.isConstructor && - isImplicitClass(sym.maybeOwner) - - if isExtensionMethod then - completionsWithSuffix( - sym, - sym.decodedName, - CompletionValue.Extension(_, _, _) - ).map(visit).forall(_ == true) - else if isImplicitClassMethod then + if completionMode.is(Mode.Scope) && query.nonEmpty then + val visitor = new CompilerSearchVisitor(sym => + indexedContext.lookupSym(sym) match + case IndexedContext.Result.InScope => false + case _ => completionsWithSuffix( sym, sym.decodedName, - CompletionValue.ImplicitClass(_, _, _, sym.maybeOwner), - ).map(visit).forall(_ == true) - else false, - ) - Some(search.searchMethods(query, buildTargetIdentifier, visitor).nn) - end match + CompletionValue.Workspace(_, _, _, sym) + ).map(visit).forall(_ == true), + ) + Some(search.search(query, buildTargetIdentifier, visitor).nn) + else if completionMode.is(Mode.Member) then + val visitor = new CompilerSearchVisitor(sym => + def isExtensionMethod = sym.is(ExtensionMethod) && + qualType.widenDealias <:< sym.extensionParam.info.widenDealias + def isImplicitClass(owner: Symbol) = + val constructorParam = + owner.info + .membersBasedOnFlags( + Flags.ParamAccessor, + Flags.EmptyFlags, + ) + .headOption + .map(_.info) + owner.isClass && owner.is(Flags.Implicit) && + constructorParam.exists(p => + qualType.widenDealias <:< p.widenDealias + ) + end isImplicitClass + + def isImplicitClassMethod = sym.is(Flags.Method) && !sym.isConstructor && + isImplicitClass(sym.maybeOwner) + + if isExtensionMethod then + completionsWithSuffix( + sym, + sym.decodedName, + CompletionValue.Extension(_, _, _) + ).map(visit).forall(_ == true) + else if isImplicitClassMethod then + completionsWithSuffix( + sym, + sym.decodedName, + CompletionValue.ImplicitClass(_, _, _, sym.maybeOwner), + ).map(visit).forall(_ == true) + else false, + ) + Some(search.searchMethods(query, buildTargetIdentifier, visitor).nn) + else + val filtered = indexedContext.scopeSymbols + .filter(sym => !sym.isConstructor && (!sym.is(Synthetic) || sym.is(Module))) + + filtered.map { sym => + visit(CompletionValue.Scope(sym.decodedName, sym, findSuffix(sym))) + } + Some(SymbolSearch.Result.INCOMPLETE) + end enrichWithSymbolSearch extension (s: SrcPos) @@ -718,8 +715,8 @@ class Completions( // show the abstract members first if !ov.symbol.is(Deferred) then penalty |= MemberOrdering.IsNotAbstract penalty - case CompletionValue.Workspace(_, sym, _, _) => - symbolRelevance(sym) | (IsWorkspaceSymbol + sym.name.show.length()) + case CompletionValue.Workspace(_, denot, _, _) => + symbolRelevance(denot.symbol) | (IsWorkspaceSymbol + denot.name.show.length()) case sym: CompletionValue.Symbolic => symbolRelevance(sym.symbol) case _ => @@ -760,11 +757,11 @@ class Completions( isMember(symbol) && symbol.owner != tpe.typeSymbol def postProcess(items: List[CompletionValue]): List[CompletionValue] = items.map { - case CompletionValue.Compiler(label, sym, suffix) - if isMember(sym) => + case completion @ CompletionValue.Compiler(label, denot, suffix) + if isMember(completion.symbol) => CompletionValue.Compiler( label, - substituteTypeVars(sym), + substituteTypeVars(completion.symbol), suffix ) case other => other diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala index 768f68a66300..95fbeec0cac0 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/InterpolatorCompletions.scala @@ -160,16 +160,16 @@ object InterpolatorCompletions: completions.completionsWithSuffix( sym, label, - (name, s, suffix) => + (name, denot, suffix) => CompletionValue.Interpolator( - s, + denot.symbol, label, Some(newText(name, suffix.toEditOpt, identOrSelect)), Nil, Some(cursor.withStart(identOrSelect.span.start).toLsp), // Needed for VS Code which will not show the completion otherwise Some(identOrSelect.name.toString() + "." + label), - s, + denot.symbol, isExtension = isExtension ), ) @@ -290,9 +290,9 @@ object InterpolatorCompletions: completions.completionsWithSuffix( sym, label, - (name, s, suffix) => + (name, denot, suffix) => CompletionValue.Interpolator( - s, + denot.symbol, label, Some(newText(name, suffix.toEditOpt)), additionalEdits(), diff --git a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala index ff7d5f7763c7..063ed2b44225 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/completions/MatchCaseCompletions.scala @@ -14,6 +14,7 @@ import dotty.tools.dotc.ast.tpd.* import dotty.tools.dotc.core.Constants.Constant import dotty.tools.dotc.core.Contexts.Context import dotty.tools.dotc.core.Definitions +import dotty.tools.dotc.core.Denotations.Denotation import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.StdNames @@ -447,7 +448,7 @@ class CompletionValueGenerator( end labelForCaseMember def toCompletionValue( - sym: Symbol, + denot: Denotation, label: String, autoImport: Option[l.TextEdit] ): CompletionValue.CaseKeyword = @@ -455,7 +456,7 @@ class CompletionValueGenerator( (if patternOnly.nonEmpty then "" else " ") + (if clientSupportsSnippets then "$0" else "") CompletionValue.CaseKeyword( - sym, + denot, label, Some(label + cursorSuffix), autoImport.toList, diff --git a/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala b/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala index 350a4c47971e..3b763523f9e6 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala @@ -7,6 +7,7 @@ import scala.meta.pc.SymbolDocumentation import scala.meta.pc.SymbolSearch import dotty.tools.dotc.core.Contexts.Context +import dotty.tools.dotc.core.Denotations.Denotation import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Flags.* import dotty.tools.dotc.core.NameKinds.ContextBoundParamName @@ -250,9 +251,10 @@ class ShortenedTypePrinter( lazy val effectiveOwner = sym.effectiveOwner sym.isType && (effectiveOwner == defn.ScalaPackageClass || effectiveOwner == defn.ScalaPredefModuleClass) - def completionSymbol(sym: Symbol): String = - val info = sym.info.widenTermRefExpr + def completionSymbol(denotation: Denotation): String = + val info = denotation.info.widenTermRefExpr val typeSymbol = info.typeSymbol + val sym = denotation.symbol lazy val typeEffectiveOwner = if typeSymbol != NoSymbol then " " + fullNameString(typeSymbol.effectiveOwner) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionCaseSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionCaseSuite.scala index b54915c56474..96a7cff9e73c 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionCaseSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionCaseSuite.scala @@ -721,4 +721,4 @@ class CompletionCaseSuite extends BaseCompletionSuite: |} |""".stripMargin, "case (Int, Int) => scala", - ) \ No newline at end of file + ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionDocSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionDocSuite.scala index bcfd399c7c4a..994dd15ea96f 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionDocSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionDocSuite.scala @@ -215,6 +215,7 @@ class CompletionDocSuite extends BaseCompletionSuite: """ |> Found documentation for scala/package.Vector. |Vector scala.collection.immutable + |Vector[A](elems: A*): CC[A] |""".stripMargin, includeDocs = true ) @@ -316,5 +317,6 @@ class CompletionDocSuite extends BaseCompletionSuite: |} """.stripMargin, """|myNumbers: Vector[Int] + |myNumbers(i: Int): A |""".stripMargin ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala index ec4fedf50f68..dc972ec22533 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionKeywordSuite.scala @@ -38,8 +38,7 @@ class CompletionKeywordSuite extends BaseCompletionSuite: | // tr@@ |} |""".stripMargin, - """|transient scala (commit: '') - |transparentTrait - scala.annotation (commit: '')""".stripMargin, + "transparentTrait - scala.annotation (commit: '')", includeCommitCharacter = true ) @@ -58,8 +57,7 @@ class CompletionKeywordSuite extends BaseCompletionSuite: | **/ |} |""".stripMargin, - """|transient scala (commit: '') - |transparentTrait - scala.annotation (commit: '')""".stripMargin, + "transparentTrait - scala.annotation (commit: '')", includeCommitCharacter = true ) @@ -414,12 +412,9 @@ class CompletionKeywordSuite extends BaseCompletionSuite: |} """.stripMargin, """|def - |deprecated scala - |deprecatedInheritance scala - |deprecatedName scala - |deprecatedOverriding scala + |derived - scala.CanEqual + |deprecated - scala.runtime.stdLibPatches.language |""".stripMargin, - topLines = Some(5) ) @Test def `protected-val` = @@ -472,10 +467,10 @@ class CompletionKeywordSuite extends BaseCompletionSuite: | def hello(u@@) |}""".stripMargin, """|using (commit: '') - |unsafeExceptions scala (commit: '') - |unchecked scala (commit: '') |unsafe - scala.caps (commit: '') |unsafeNulls - scala.runtime.stdLibPatches.language (commit: '') + |unused - scala.annotation (commit: '') + |unshared - scala.annotation.internal (commit: '') |""".stripMargin, includeCommitCharacter = true, topLines = Some(5) @@ -486,11 +481,11 @@ class CompletionKeywordSuite extends BaseCompletionSuite: """|object A{ | def hello(a: String, u@@) |}""".stripMargin, - """|unsafeExceptions scala - |unchecked scala - |unsafe - scala.caps + """|unsafe - scala.caps |unsafeNulls - scala.runtime.stdLibPatches.language - |unused - scala.annotation """.stripMargin, + |unused - scala.annotation + |unshared - scala.annotation.internal + |unspecialized - scala.annotation""".stripMargin, topLines = Some(5) ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala index 619ba523de0a..df6a3f705be9 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionPatternSuite.scala @@ -54,8 +54,7 @@ class CompletionPatternSuite extends BaseCompletionSuite: | case ma@@ | } |}""".stripMargin, - """|main scala - |macros - scala.languageFeature.experimental + """|macros - scala.languageFeature.experimental |macroImpl - scala.reflect.macros.internal |""".stripMargin ) diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSnippetSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSnippetSuite.scala index 3091ddc32b3a..c3e3f374c23d 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSnippetSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSnippetSuite.scala @@ -3,6 +3,7 @@ package dotty.tools.pc.tests.completion import dotty.tools.pc.base.BaseCompletionSuite import org.junit.Test +import org.junit.Ignore class CompletionSnippetSuite extends BaseCompletionSuite: @@ -76,6 +77,7 @@ class CompletionSnippetSuite extends BaseCompletionSuite: // Dotty does not currently support fuzzy completions. Please take a look at // https://github.com/lampepfl/dotty-feature-requests/issues/314 + @Ignore("Fuzzy should be provided by dotty") @Test def `type-empty` = if (scala.util.Properties.isJavaAtLeast("9")) { checkSnippet( @@ -105,6 +107,7 @@ class CompletionSnippetSuite extends BaseCompletionSuite: // Dotty does not currently support fuzzy completions. Please take a look at // https://github.com/lampepfl/dotty-feature-requests/issues/314 + @Ignore("Fuzzy should be provided by dotty") @Test def `type-new-empty` = if (scala.util.Properties.isJavaAtLeast("9")) { checkSnippet( diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala index d1d382b17ca7..274320276a56 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -7,6 +7,7 @@ import dotty.tools.pc.base.BaseCompletionSuite import dotty.tools.pc.utils.MockEntries import org.junit.Test +import org.junit.Ignore class CompletionSuite extends BaseCompletionSuite: @@ -26,10 +27,10 @@ class CompletionSuite extends BaseCompletionSuite: |}""".stripMargin, """ |List scala.collection.immutable + |List[A](elems: A*): CC[A] |List - java.awt |List - java.util - |List - scala.collection.immutable - |List[A](elems: A*): CC[A] + |ListMap[K, V](elems: (K, V)*): CC[K, V] |""".stripMargin, topLines = Some(5) ) @@ -109,17 +110,17 @@ class CompletionSuite extends BaseCompletionSuite: |tabulate[A](n: Int)(f: Int => A): List[A] |unapplySeq[A](x: List[A] @uncheckedVariance): UnapplySeqWrapper[A] |unfold[A, S](init: S)(f: S => Option[(A, S)]): List[A] - |->[B](y: B): (A, B) - |ensuring(cond: Boolean): A - |ensuring(cond: A => Boolean): A - |ensuring(cond: Boolean, msg: => Any): A - |ensuring(cond: A => Boolean, msg: => Any): A - |fromSpecific(from: From)(it: IterableOnce[A]): C - |fromSpecific(it: IterableOnce[A]): C - |nn: x.type & T - |toFactory(from: From): Factory[A, C] + |->[B](y: B): (List.type, B) + |ensuring(cond: Boolean): List.type + |ensuring(cond: List.type => Boolean): List.type + |ensuring(cond: Boolean, msg: => Any): List.type + |ensuring(cond: List.type => Boolean, msg: => Any): List.type + |fromSpecific(from: Any)(it: IterableOnce[Nothing]): List[Nothing] + |fromSpecific(it: IterableOnce[Nothing]): List[Nothing] + |nn: List.type & List.type + |toFactory(from: Any): Factory[Nothing, List[Nothing]] |formatted(fmtstr: String): String - |→[B](y: B): (A, B) + |→[B](y: B): (List.type, B) |iterableFactory[A]: Factory[A, List[A]] |asInstanceOf[X0]: X0 |equals(x$0: Any): Boolean @@ -146,6 +147,7 @@ class CompletionSuite extends BaseCompletionSuite: "XtensionMethod(a: Int): XtensionMethod" ) + @Ignore("This test should be handled by compiler fuzzy search") @Test def fuzzy = check( """ @@ -157,14 +159,14 @@ class CompletionSuite extends BaseCompletionSuite: |""".stripMargin ) + @Ignore("This test should be handled by compiler fuzzy search") @Test def fuzzy1 = check( """ |object A { - | new PBuil@@ + | new PBuilder@@ |}""".stripMargin, - """|ProcessBuilder java.lang - |ProcessBuilder - scala.sys.process + """|ProcessBuilder - scala.sys.process |ProcessBuilderImpl - scala.sys.process |""".stripMargin, filter = _.contains("ProcessBuilder") @@ -515,8 +517,8 @@ class CompletionSuite extends BaseCompletionSuite: """.stripMargin, """|until(end: Int): Range |until(end: Int, step: Int): Range - |until(end: T): Exclusive[T] - |until(end: T, step: T): Exclusive[T] + |until(end: Long): Exclusive[Long] + |until(end: Long, step: Long): Exclusive[Long] |""".stripMargin, postProcessObtained = _.replace("Float", "Double"), stableOrder = false @@ -1187,7 +1189,7 @@ class CompletionSuite extends BaseCompletionSuite: | val x = Bar[M](new Foo[Int]{}) | x.bar.m@@ |""".stripMargin, - """|map[B](f: A => B): Foo[B] + """|map[B](f: Int => B): Foo[B] |""".stripMargin, topLines = Some(1) ) @@ -1672,7 +1674,6 @@ class CompletionSuite extends BaseCompletionSuite: topLines = Some(5) ) - @Test def `import-rename` = check( """import scala.collection.{AbstractMap => Set@@} @@ -1680,6 +1681,7 @@ class CompletionSuite extends BaseCompletionSuite: "" ) + @Ignore @Test def `dont-crash-implicit-search` = check( """object M: @@ -1687,3 +1689,26 @@ class CompletionSuite extends BaseCompletionSuite: |""".stripMargin, "" ) + + @Test def `extension-definition-type-variable-inference` = + check( + """|object M: + | extension [T](xs: List[T]) def test(p: T => Boolean): List[T] = ??? + | List(1,2,3).tes@@ + |""".stripMargin, + """|test(p: Int => Boolean): List[Int] + |""".stripMargin + ) + + @Test def `old-style-extension-type-variable-inference` = + check( + """|object M: + | implicit class ListUtils[T](xs: List[T]) { + | def test(p: T => Boolean): List[T] = ??? + | } + | List(1,2,3).tes@@ + |""".stripMargin, + """|test(p: Int => Boolean): List[Int] + |""".stripMargin + ) + diff --git a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala index 98d164db87f1..706ae5062308 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala @@ -773,7 +773,7 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: |""".stripMargin ) - @Test def `case_class_param` = + @Test def `case-class-param` = check( """|case class Foo(fooBar: Int, gooBar: Int) |class Bar(val fooBaz: Int, val fooBal: Int) { @@ -790,6 +790,7 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: |""".stripMargin, """|fooBar: String |fooBar: List[Int] + |fooBar(n: Int): A |""".stripMargin, ) @@ -825,7 +826,7 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: |MyType - demo.other""".stripMargin, ) - @Test def `method-name-conflict` = + @Test def `method-name-conflict` = checkEdit( """|package demo | diff --git a/presentation-compiler/test/dotty/tools/pc/utils/PcAssertions.scala b/presentation-compiler/test/dotty/tools/pc/utils/PcAssertions.scala index 5efb0feaeb9a..6dfc8acec66c 100644 --- a/presentation-compiler/test/dotty/tools/pc/utils/PcAssertions.scala +++ b/presentation-compiler/test/dotty/tools/pc/utils/PcAssertions.scala @@ -128,11 +128,11 @@ trait PcAssertions: val lines = diff.linesIterator.toList val sources = completionSources.padTo(lines.size, CompletionSource.Empty) val maxLength = lines.map(_.length).maxOption.getOrElse(0) - var redLineIndex = 0 + var completionIndex = 0 lines.map: line => - if line.startsWith(Console.BOLD + Console.RED) then - redLineIndex = redLineIndex + 1 - s"$line | [${sources(redLineIndex - 1)}]" + if line.startsWith(Console.BOLD + Console.RED) || line.startsWith(" ") then + completionIndex += 1 + s"$line | [${sources(completionIndex - 1)}]" else line .mkString("\n")