diff --git a/compiler/src/dotty/tools/dotc/interactive/Completion.scala b/compiler/src/dotty/tools/dotc/interactive/Completion.scala index e4d0cce9f6f9..8fb844f1f333 100644 --- a/compiler/src/dotty/tools/dotc/interactive/Completion.scala +++ b/compiler/src/dotty/tools/dotc/interactive/Completion.scala @@ -1,8 +1,7 @@ package dotty.tools.dotc.interactive -import scala.language.unsafeNulls - import dotty.tools.dotc.ast.untpd +import dotty.tools.dotc.ast.NavigateAST import dotty.tools.dotc.config.Printers.interactiv import dotty.tools.dotc.core.Contexts._ import dotty.tools.dotc.core.Decorators._ @@ -25,6 +24,10 @@ import dotty.tools.dotc.util.SourcePosition import scala.collection.mutable import scala.util.control.NonFatal +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 /** * One of the results of a completion query. @@ -37,7 +40,7 @@ import scala.util.control.NonFatal */ case class Completion(label: String, description: String, symbols: List[Symbol]) -object Completion { +object Completion: import dotty.tools.dotc.ast.tpd._ @@ -45,10 +48,9 @@ object Completion { * * @return offset and list of symbols for possible completions */ - def completions(pos: SourcePosition)(using Context): (Int, List[Completion]) = { - val path = Interactive.pathTo(ctx.compilationUnit.tpdTree, pos.span) + 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)) - } /** * Inspect `path` to determine what kinds of symbols should be considered. @@ -60,10 +62,11 @@ object Completion { * * Otherwise, provide no completion suggestion. */ - def completionMode(path: List[Tree], pos: SourcePosition): Mode = - path match { - case Ident(_) :: Import(_, _) :: _ => Mode.ImportOrExport - case (ref: RefTree) :: _ => + 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 @@ -72,9 +75,8 @@ object Completion { if sel.imported.span.contains(pos.span) then Mode.ImportOrExport else Mode.None // Can't help completing the renaming - case (_: ImportOrExport) :: _ => Mode.ImportOrExport + case (_: untpd.ImportOrExport) :: _ => Mode.ImportOrExport case _ => Mode.None - } /** 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 @@ -101,10 +103,13 @@ object Completion { case (sel: untpd.ImportSelector) :: _ => completionPrefix(sel.imported :: Nil, pos) + case untpd.Ident(_) :: (sel: untpd.ImportSelector) :: _ if !sel.isGiven => + completionPrefix(sel.imported :: Nil, pos) + case (tree: untpd.ImportOrExport) :: _ => - tree.selectors.find(_.span.contains(pos.span)).map { selector => + tree.selectors.find(_.span.contains(pos.span)).map: selector => completionPrefix(selector :: Nil, pos) - }.getOrElse("") + .getOrElse("") // Foo.`se will result in Select(Ident(Foo), ) case (select: untpd.Select) :: _ if select.name == nme.ERROR => @@ -118,27 +123,65 @@ object Completion { if (ref.name == nme.ERROR) "" else ref.name.toString.take(pos.span.point - ref.span.point) - case _ => - "" + case _ => "" + end completionPrefix /** Inspect `path` to determine the offset where the completion result should be inserted. */ - def completionOffset(path: List[Tree]): Int = - path match { - case (ref: RefTree) :: _ => ref.span.point + def completionOffset(untpdPath: List[untpd.Tree]): Int = + untpdPath match { + case (ref: untpd.RefTree) :: _ => ref.span.point case _ => 0 } - private def computeCompletions(pos: SourcePosition, path: List[Tree])(using Context): (Int, List[Completion]) = { - val mode = completionMode(path, pos) - val rawPrefix = completionPrefix(path, pos) + /** 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 + * are no longer a part of a typed tree, but instead are prepended to method parameters. + * + * @param untpdPath The typed or untyped path to the tree that is being completed + * @param tpdPath The typed path that will be returned if no extension method construct is found + * @param pos The cursor position + * + * @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.collectFirst: + case untpd.ExtMethods(paramss, _) => + val enclosingParam = paramss.flatten.find(_.span.contains(pos.span)) + enclosingParam.map: param => + ctx.typer.index(paramss.flatten) + val typedEnclosingParam = ctx.typer.typed(param) + 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) val hasBackTick = rawPrefix.headOption.contains('`') val prefix = if hasBackTick then rawPrefix.drop(1) else rawPrefix val completer = new Completer(mode, prefix, pos) - val completions = path match { + 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 @@ -147,13 +190,12 @@ object Completion { case (tree: ImportOrExport) :: _ => completer.directMemberCompletions(tree.expr) case (_: untpd.ImportSelector) :: Import(expr, _) :: _ => completer.directMemberCompletions(expr) case _ => completer.scopeCompletions - } val describedCompletions = describeCompletions(completions) val backtickedCompletions = describedCompletions.map(completion => backtickCompletions(completion, hasBackTick)) - val offset = completionOffset(path) + val offset = completionOffset(path0) interactiv.println(i"""completion with pos = $pos, | prefix = ${completer.prefix}, @@ -161,7 +203,6 @@ object Completion { | type = ${completer.mode.is(Mode.Type)} | results = $backtickedCompletions%, %""") (offset, backtickedCompletions) - } def backtickCompletions(completion: Completion, hasBackTick: Boolean) = if hasBackTick || needsBacktick(completion.label) then @@ -174,17 +215,17 @@ object Completion { // https://github.com/scalameta/metals/blob/main/mtags/src/main/scala/scala/meta/internal/mtags/KeywordWrapper.scala // https://github.com/com-lihaoyi/Ammonite/blob/73a874173cd337f953a3edc9fb8cb96556638fdd/amm/util/src/main/scala/ammonite/util/Model.scala private def needsBacktick(s: String) = - val chunks = s.split("_", -1) + val chunks = s.split("_", -1).nn val validChunks = chunks.zipWithIndex.forall { case (chunk, index) => - chunk.forall(Chars.isIdentifierPart) || - (chunk.forall(Chars.isOperatorPart) && + chunk.nn.forall(Chars.isIdentifierPart) || + (chunk.nn.forall(Chars.isOperatorPart) && index == chunks.length - 1 && !(chunks.lift(index - 1).contains("") && index - 1 == 0)) } val validStart = - Chars.isIdentifierStart(s(0)) || chunks(0).forall(Chars.isOperatorPart) + Chars.isIdentifierStart(s(0)) || chunks(0).nn.forall(Chars.isOperatorPart) val valid = validChunks && validStart && !keywords.contains(s) @@ -216,7 +257,7 @@ object Completion { * For the results of all `xyzCompletions` methods term names and type names are always treated as different keys in the same map * and they never conflict with each other. */ - class Completer(val mode: Mode, val prefix: String, pos: SourcePosition) { + class Completer(val mode: Mode, val prefix: String, pos: SourcePosition): /** Completions for terms and types that are currently in scope: * the members of the current class, local definitions and the symbols that have been imported, * recursively adding completions from outer scopes. @@ -230,7 +271,7 @@ object Completion { * (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 = { + def scopeCompletions(using context: Context): CompletionMap = val mappings = collection.mutable.Map.empty[Name, List[ScopedDenotations]].withDefaultValue(List.empty) def addMapping(name: Name, denots: ScopedDenotations) = mappings(name) = mappings(name) :+ denots @@ -302,7 +343,7 @@ object Completion { } resultMappings - } + end scopeCompletions /** Widen only those types which are applied or are exactly nothing */ @@ -335,16 +376,16 @@ object Completion { /** Completions introduced by imports directly in this context. * Completions from outer contexts are not included. */ - private def importedCompletions(using Context): CompletionMap = { + private def importedCompletions(using Context): CompletionMap = val imp = ctx.importInfo - def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] = - imp.site.member(name).alternatives - .collect { case denot if include(denot, nameInScope) => nameInScope -> denot } - if imp == null then Map.empty else + def fromImport(name: Name, nameInScope: Name): Seq[(Name, SingleDenotation)] = + imp.site.member(name).alternatives + .collect { case denot if include(denot, nameInScope) => nameInScope -> denot } + val givenImports = imp.importedImplicits .map { ref => (ref.implicitName: Name, ref.underlyingRef.denot.asSingleDenotation) } .filter((name, denot) => include(denot, name)) @@ -370,7 +411,7 @@ object Completion { }.toSeq.groupByName givenImports ++ wildcardMembers ++ explicitMembers - } + end importedCompletions /** Completions from implicit conversions including old style extensions using implicit classes */ private def implicitConversionMemberCompletions(qual: Tree)(using Context): CompletionMap = @@ -532,7 +573,6 @@ object Completion { extension [N <: Name](namedDenotations: Seq[(N, SingleDenotation)]) @annotation.targetName("groupByNameTupled") def groupByName: CompletionMap = namedDenotations.groupMap((name, denot) => name)((name, denot) => denot) - } private type CompletionMap = Map[Name, Seq[SingleDenotation]] @@ -545,11 +585,11 @@ object Completion { * The completion mode: defines what kinds of symbols should be included in the completion * results. */ - class Mode(val bits: Int) extends AnyVal { + 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) - } - object Mode { + + object Mode: /** No symbol should be included */ val None: Mode = new Mode(0) @@ -561,6 +601,4 @@ object Completion { /** Both term and type symbols are allowed */ val ImportOrExport: Mode = new Mode(4) | Term | Type - } -} diff --git a/compiler/src/dotty/tools/repl/ReplCompiler.scala b/compiler/src/dotty/tools/repl/ReplCompiler.scala index 764695e8479b..d3a5561b6080 100644 --- a/compiler/src/dotty/tools/repl/ReplCompiler.scala +++ b/compiler/src/dotty/tools/repl/ReplCompiler.scala @@ -93,9 +93,9 @@ class ReplCompiler extends Compiler: end compile final def typeOf(expr: String)(using state: State): Result[String] = - typeCheck(expr).map { tree => + typeCheck(expr).map { (_, tpdTree) => given Context = state.context - tree.rhs match { + tpdTree.rhs match { case Block(xs, _) => xs.last.tpe.widen.show case _ => """Couldn't compute the type of your expression, so sorry :( @@ -129,7 +129,7 @@ class ReplCompiler extends Compiler: Iterator(sym) ++ sym.allOverriddenSymbols } - typeCheck(expr).map { + typeCheck(expr).map { (_, tpdTree) => tpdTree match case ValDef(_, _, Block(stats, _)) if stats.nonEmpty => val stat = stats.last.asInstanceOf[tpd.Tree] if (stat.tpe.isError) stat.tpe.show @@ -152,7 +152,7 @@ class ReplCompiler extends Compiler: } } - final def typeCheck(expr: String, errorsAllowed: Boolean = false)(using state: State): Result[tpd.ValDef] = { + final def typeCheck(expr: String, errorsAllowed: Boolean = false)(using state: State): Result[(untpd.ValDef, tpd.ValDef)] = { def wrapped(expr: String, sourceFile: SourceFile, state: State)(using Context): Result[untpd.PackageDef] = { def wrap(trees: List[untpd.Tree]): untpd.PackageDef = { @@ -181,22 +181,32 @@ class ReplCompiler extends Compiler: } } - def unwrapped(tree: tpd.Tree, sourceFile: SourceFile)(using Context): Result[tpd.ValDef] = { - def error: Result[tpd.ValDef] = - List(new Diagnostic.Error(s"Invalid scala expression", - sourceFile.atSpan(Span(0, sourceFile.content.length)))).errors + def error[Tree <: untpd.Tree](sourceFile: SourceFile): Result[Tree] = + List(new Diagnostic.Error(s"Invalid scala expression", + sourceFile.atSpan(Span(0, sourceFile.content.length)))).errors + def unwrappedTypeTree(tree: tpd.Tree, sourceFile0: SourceFile)(using Context): Result[tpd.ValDef] = { import tpd._ tree match { case PackageDef(_, List(TypeDef(_, tmpl: Template))) => tmpl.body .collectFirst { case dd: ValDef if dd.name.show == "expr" => dd.result } - .getOrElse(error) + .getOrElse(error[tpd.ValDef](sourceFile0)) case _ => - error + error[tpd.ValDef](sourceFile0) } } + def unwrappedUntypedTree(tree: untpd.Tree, sourceFile0: SourceFile)(using Context): Result[untpd.ValDef] = + import untpd._ + tree match { + case PackageDef(_, List(TypeDef(_, tmpl: Template))) => + tmpl.body + .collectFirst { case dd: ValDef if dd.name.show == "expr" => dd.result } + .getOrElse(error[untpd.ValDef](sourceFile0)) + case _ => + error[untpd.ValDef](sourceFile0) + } val src = SourceFile.virtual("", expr) inContext(state.context.fresh @@ -209,7 +219,10 @@ class ReplCompiler extends Compiler: ctx.run.nn.compileUnits(unit :: Nil, ctx) if (errorsAllowed || !ctx.reporter.hasErrors) - unwrapped(unit.tpdTree, src) + for + tpdTree <- unwrappedTypeTree(unit.tpdTree, src) + untpdTree <- unwrappedUntypedTree(unit.untpdTree, src) + yield untpdTree -> tpdTree else ctx.reporter.removeBufferedMessages.errors } diff --git a/compiler/src/dotty/tools/repl/ReplDriver.scala b/compiler/src/dotty/tools/repl/ReplDriver.scala index 905f4f06de08..2471f6bece42 100644 --- a/compiler/src/dotty/tools/repl/ReplDriver.scala +++ b/compiler/src/dotty/tools/repl/ReplDriver.scala @@ -251,10 +251,11 @@ class ReplDriver(settings: Array[String], given state: State = newRun(state0) compiler .typeCheck(expr, errorsAllowed = true) - .map { tree => + .map { (untpdTree, tpdTree) => val file = SourceFile.virtual("", expr, maybeIncomplete = true) val unit = CompilationUnit(file)(using state.context) - unit.tpdTree = tree + unit.untpdTree = untpdTree + unit.tpdTree = tpdTree given Context = state.context.fresh.setCompilationUnit(unit) val srcPos = SourcePosition(file, Span(cursor)) val completions = try Completion.completions(srcPos)._2 catch case NonFatal(_) => Nil diff --git a/compiler/test/dotty/tools/repl/TabcompleteTests.scala b/compiler/test/dotty/tools/repl/TabcompleteTests.scala index 910584a9b5e7..0bce525e1469 100644 --- a/compiler/test/dotty/tools/repl/TabcompleteTests.scala +++ b/compiler/test/dotty/tools/repl/TabcompleteTests.scala @@ -32,6 +32,11 @@ class TabcompleteTests extends ReplTest { assertEquals(List("apply"), comp) } + @Test def tabCompleteInExtensionDefinition = initially { + val comp = tabComplete("extension (x: Lis") + assertEquals(List("List"), comp) + } + @Test def tabCompleteTwiceIn = { val src1 = "class Foo { def bar(xs: List[Int]) = xs.map" val src2 = "class Foo { def bar(xs: List[Int]) = xs.mapC" diff --git a/language-server/test/dotty/tools/languageserver/CompletionTest.scala b/language-server/test/dotty/tools/languageserver/CompletionTest.scala index 4dd9c276b8b4..7a21fe15fbe5 100644 --- a/language-server/test/dotty/tools/languageserver/CompletionTest.scala +++ b/language-server/test/dotty/tools/languageserver/CompletionTest.scala @@ -1523,9 +1523,72 @@ class CompletionTest { |object Test: | def foo: ArrayBuffer[Fo${m1}] = ??? """ + .completion(m1, Set(("Foo",Class,"Foo"))) + } + + @Test def extensionDefinitionCompletions: Unit = + code"""|trait Foo + |object T: + | extension (x: Fo$m1) + |""" + .completion(m1, Set(("Foo",Class,"Foo"))) + + @Test def extensionDefinitionCompletionsSelect: Unit = + code"""|object Test: + | class TestSelect() + |object T: + | extension (x: Test.TestSel$m1) + |""" .completion(m1, Set( - ("Foo",Class,"Foo") - ) - ) - } + ("TestSelect", Module, "Test.TestSelect"), ("TestSelect", Class, "Test.TestSelect") + )) + + @Test def extensionDefinitionCompletionsSelectNested: Unit = + code"""|object Test: + | object Test2: + | class TestSelect() + |object T: + | extension (x: Test.Test2.TestSel$m1) + |""" + .completion(m1, Set( + ("TestSelect", Module, "Test.Test2.TestSelect"), ("TestSelect", Class, "Test.Test2.TestSelect") + )) + + @Test def extensionDefinitionCompletionsSelectInside: Unit = + code"""|object Test: + | object Test2: + | class TestSelect() + |object T: + | extension (x: Test.Te$m1.TestSelect) + |""" + .completion(m1, Set(("Test2", Module, "Test.Test2"))) + + @Test def extensionDefinitionCompletionsTypeParam: Unit = + code"""|object T: + | extension [TypeParam](x: TypePar$m1) + |""" + .completion(m1, Set(("TypeParam", Field, "T.TypeParam"))) + + + @Test def typeParamCompletions: Unit = + code"""|object T: + | def xxx[TTT](x: TT$m1) + |""" + .completion(m1, Set(("TTT", Field, "T.TTT"))) + + @Test def selectDynamic: Unit = + code"""|import scala.language.dynamics + |class Foo extends Dynamic { + | def banana: Int = 42 + | def selectDynamic(field: String): Foo = this + | def applyDynamicNamed(name: String)(arg: (String, Int)): Foo = this + | def updateDynamic(name: String)(value: Int): Foo = this + |} + |object Test: + | val x = new Foo() + | x.sele$m1 + | x.bana$m2 + |""" + .completion(m1, Set(("selectDynamic", Method, "(field: String): Foo"))) + .completion(m2, Set(("banana", Method, "=> Int"))) } 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 0574aa2cdb7d..64bbbb848289 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,7 @@ import scala.meta.internal.pc.{IdentifierComparator, MemberOrdering} import scala.meta.pc.* import dotty.tools.dotc.ast.tpd.* +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.* @@ -24,6 +25,7 @@ import dotty.tools.dotc.core.StdNames.* import dotty.tools.dotc.core.Symbols.* import dotty.tools.dotc.core.Types.* import dotty.tools.dotc.interactive.Completion +import dotty.tools.dotc.interactive.Completion.Mode import dotty.tools.dotc.transform.SymUtils.* import dotty.tools.dotc.util.SourcePosition import dotty.tools.dotc.util.Spans @@ -54,6 +56,13 @@ class Completions( val coursierComplete = new CoursierComplete(BuildInfo.scalaVersion) + private lazy val completionMode = + val adjustedPath = Completion.resolveTypedOrUntypedPath(path, pos) + val mode = Completion.completionMode(adjustedPath, pos) + path match + case Literal(Constant(_: String)) :: _ => Mode.Term // literal completions + case _ => mode + private lazy val shouldAddSnippet = path match /* In case of `method@@()` we should not add snippets and the path @@ -69,113 +78,38 @@ class Completions( case (_: Ident) :: (_: SeqLiteral) :: _ => false case _ => true - enum CursorPos: - case Type(hasTypeParams: Boolean, hasNewKw: Boolean) - case Term - case Import - - def include(sym: Symbol)(using Context): Boolean = - def hasSyntheticCursorSuffix: Boolean = - if !sym.name.endsWith(Cursor.value) then false - else - val realNameLength = sym.decodedName.length - Cursor.value.length - sym.source == pos.source && - sym.span.start + realNameLength == pos.span.end - - val generalExclude = - isUninterestingSymbol(sym) || - !isNotLocalForwardReference(sym) || - sym.isPackageObject || - hasSyntheticCursorSuffix - - def isWildcardParam(sym: Symbol) = - if sym.isTerm && sym.owner.isAnonymousFunction then - sym.name match - case DerivedName(under, _) => - under.isEmpty - case _ => false - else false + private lazy val allowTemplateSuffix: Boolean = + path match + case _ :: New(selectOrIdent: (Select | Ident)) :: _ => true + case _ => false - if generalExclude then false + def includeSymbol(sym: Symbol)(using Context): Boolean = + def hasSyntheticCursorSuffix: Boolean = + if !sym.name.endsWith(Cursor.value) then false else - this match - case Type(_, _) => true - case Term if isWildcardParam(sym) => false - case Term if sym.isTerm || sym.is(Package) => true - case Import => true + val realNameLength = sym.decodedName.length - Cursor.value.length + sym.source == pos.source && + sym.span.start + realNameLength == pos.span.end + + val generalExclude = + isUninterestingSymbol(sym) || + !isNotLocalForwardReference(sym) || + sym.isPackageObject || + hasSyntheticCursorSuffix + + def isWildcardParam(sym: Symbol) = + if sym.isTerm && sym.owner.isAnonymousFunction then + sym.name match + case DerivedName(under, _) => + under.isEmpty case _ => false - end if - end include + else false - def allowBracketSuffix: Boolean = - this match - case Type(hasTypeParams, _) => !hasTypeParams - case _ => false - - def allowTemplateSuffix: Boolean = - this match - case Type(_, hasNewKw) => hasNewKw - case _ => false - - def allowApplicationSuffix: Boolean = - this match - case Term => true - case _ => false - - end CursorPos - - private lazy val cursorPos = - calculateTypeInstanceAndNewPositions(path) - - private def calculateTypeInstanceAndNewPositions( - path: List[Tree] - ): CursorPos = - path match - case (_: Import) :: _ => CursorPos.Import - case _ :: (_: Import) :: _ => CursorPos.Import - case (head: (Select | Ident)) :: tail => - // https://github.com/lampepfl/dotty/issues/15750 - // due to this issue in dotty, because of which trees after typer lose information, - // we have to calculate hasNoSquareBracket manually: - val hasSquareBracket = - val span: Span = head.srcPos.span - if span.exists then - var i = span.end - while i < (text.length() - 1) && text(i).isWhitespace do i = i + 1 - - if i < text.length() then text(i) == '[' - else false - else false - - def typePos = CursorPos.Type(hasSquareBracket, hasNewKw = false) - def newTypePos = - CursorPos.Type(hasSquareBracket, hasNewKw = true) - - tail match - case (v: ValOrDefDef) :: _ if v.tpt.sourcePos.contains(pos) => - typePos - case New(selectOrIdent: (Select | Ident)) :: _ - if selectOrIdent.sourcePos.contains(pos) => - newTypePos - case (a @ AppliedTypeTree(_, args)) :: _ - if args.exists(_.sourcePos.contains(pos)) => - typePos - case (templ @ Template(constr, _, self, _)) :: _ - if (constr :: self :: templ.parents).exists( - _.sourcePos.contains(pos) - ) => - typePos - case _ => - CursorPos.Term - end match - - case (_: TypeTree) :: TypeApply(Select(newQualifier: New, _), _) :: _ - if newQualifier.sourcePos.contains(pos) => - CursorPos.Type(hasTypeParams = false, hasNewKw = true) - - case _ => CursorPos.Term - end match - end calculateTypeInstanceAndNewPositions + if generalExclude then false + else if completionMode.is(Mode.Type) then true + else !isWildcardParam(sym) && (sym.isTerm || sym.is(Package)) + end if + end includeSymbol def completions(): (List[CompletionValue], SymbolSearch.Result) = val (advanced, exclusive) = advancedCompletions(path, pos, completionPos) @@ -206,7 +140,7 @@ class Completions( end match val application = CompletionApplication.fromPath(path) - val ordering = completionOrdering(application, cursorPos) + val ordering = completionOrdering(application) val values = application.postProcess(all.sorted(ordering)) (values, result) end completions @@ -256,8 +190,7 @@ class Completions( private def findSuffix(symbol: Symbol): CompletionSuffix = CompletionSuffix.empty .chain { suffix => // for [] suffix - if shouldAddSnippet && - cursorPos.allowBracketSuffix && symbol.info.typeParams.nonEmpty + if shouldAddSnippet && symbol.info.typeParams.nonEmpty then suffix.withNewSuffixSnippet(SuffixKind.Bracket) else suffix } @@ -285,7 +218,7 @@ class Completions( else suffix } .chain { suffix => // for {} suffix - if shouldAddSnippet && cursorPos.allowTemplateSuffix + if shouldAddSnippet && allowTemplateSuffix && isAbstractType(symbol) then if suffix.hasSnippet then suffix.withNewSuffix(SuffixKind.Template) @@ -305,9 +238,8 @@ class Completions( def companionSynthetic = sym.companion.exists && sym.companion.is(Synthetic) // find the apply completion that would need a snippet val methodSymbols = - if shouldAddSnippet && - (sym.is(Flags.Module) || sym.isClass && !sym.is(Flags.Trait)) && - !sym.is(Flags.JavaDefined) && cursorPos.allowApplicationSuffix + if shouldAddSnippet && completionMode.is(Mode.Term) && + (sym.is(Flags.Module) || sym.isClass && !sym.is(Flags.Trait)) && !sym.is(Flags.JavaDefined) then val info = /* Companion will be added even for normal classes now, @@ -635,7 +567,7 @@ class Completions( val suffix = if symOnly.snippetSuffix.addLabelSnippet then "[]" else "" val id = nameId + suffix - val include = cursorPos.include(sym) + val include = includeSymbol(sym) (id, include) case kw: CompletionValue.Keyword => (kw.label, true) case mc: CompletionValue.MatchCompletion => (mc.label, true) @@ -695,7 +627,6 @@ class Completions( private def computeRelevancePenalty( completion: CompletionValue, application: CompletionApplication, - cursorPos: CursorPos, ): Int = import scala.meta.internal.pc.MemberOrdering.* @@ -741,10 +672,8 @@ class Completions( relevance |= IsSynthetic if sym.isDeprecated then relevance |= IsDeprecated if isEvilMethod(sym.name) then relevance |= IsEvilMethod - cursorPos match - case CursorPos.Type(_, _) if !sym.isType => - relevance |= IsNotTypeInTypePos - case _ => + if !completionMode.is(Mode.ImportOrExport) && + completionMode.is(Mode.Type) && !sym.isType then relevance |= IsNotTypeInTypePos relevance end symbolRelevance @@ -822,8 +751,7 @@ class Completions( end CompletionApplication private def completionOrdering( - application: CompletionApplication, - cursorPos: CursorPos, + application: CompletionApplication ): Ordering[CompletionValue] = new Ordering[CompletionValue]: val queryLower = completionPos.query.toLowerCase() @@ -838,8 +766,8 @@ class Completions( def compareByRelevance(o1: CompletionValue, o2: CompletionValue): Int = Integer.compare( - computeRelevancePenalty(o1, application, cursorPos), - computeRelevancePenalty(o2, application, cursorPos), + computeRelevancePenalty(o1, application), + computeRelevancePenalty(o2, application), ) def fuzzyScore(o: CompletionValue.Symbolic): Int = 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 088ecd6c3a0c..5652fd0d9bcc 100644 --- a/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala +++ b/presentation-compiler/src/main/dotty/tools/pc/printer/ShortenedTypePrinter.scala @@ -234,18 +234,25 @@ class ShortenedTypePrinter( end match end hoverSymbol + def isImportedByDefault(sym: Symbol): Boolean = + import dotty.tools.dotc.core.Symbols.defn + lazy val effectiveOwner = sym.effectiveOwner + sym.isType && (effectiveOwner == defn.ScalaPackageClass || effectiveOwner == defn.ScalaPredefModuleClass) + def completionSymbol(sym: Symbol): String = val info = sym.info.widenTermRefExpr val typeSymbol = info.typeSymbol - if sym.is(Flags.Package) || sym.isClass then " " + fullNameString(sym.effectiveOwner) - else if sym.is(Flags.Module) || typeSymbol.is(Flags.Module) then + lazy val typeEffectiveOwner = if typeSymbol != NoSymbol then " " + fullNameString(typeSymbol.effectiveOwner) else " " + fullNameString(sym.effectiveOwner) + + if isImportedByDefault(sym) then typeEffectiveOwner + else if sym.is(Flags.Package) || sym.isClass then " " + fullNameString(sym.effectiveOwner) + else if sym.is(Flags.Module) || typeSymbol.is(Flags.Module) then typeEffectiveOwner else if sym.is(Flags.Method) then defaultMethodSignature(sym, info, onlyMethodParams = true) - else if sym.isType - then + else if sym.isType then info match case TypeAlias(t) => " = " + tpe(t.resultType) case t => tpe(t.resultType) 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 313013c34de1..bcd47259bb57 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionSuite.scala @@ -539,7 +539,7 @@ class CompletionSuite extends BaseCompletionSuite: | new Foo().bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic2 = @@ -549,7 +549,7 @@ class CompletionSuite extends BaseCompletionSuite: | val x = new Foo().foo.bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic3 = @@ -560,7 +560,7 @@ class CompletionSuite extends BaseCompletionSuite: | (foo.bar = 42).bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic4 = @@ -570,7 +570,7 @@ class CompletionSuite extends BaseCompletionSuite: | val foo = new Foo().foo(x = 42).bana@@ |} |""".stripMargin, - "selectDynamic(field: String): Foo" + "banana: Int" ) @Test def dynamic5 = @@ -669,14 +669,12 @@ class CompletionSuite extends BaseCompletionSuite: check( s"""|object Main { | Option(1) match { - | case _: S@@ + | case _: Som@@ |} |""".stripMargin, """|Some[?] scala - |Seq scala.collection.immutable - |Set scala.collection.immutable |""".stripMargin, - topLines = Some(3) + topLines = Some(1) ) @Test def adt3 = @@ -695,9 +693,8 @@ class CompletionSuite extends BaseCompletionSuite: |""".stripMargin, """|NotString: Int |Number: Regex - |Nil scala.collection.immutable |""".stripMargin, - topLines = Option(3) + topLines = Some(2) ) @Test def adt4 = @@ -705,29 +702,24 @@ class CompletionSuite extends BaseCompletionSuite: s"""|object Main { | val Number = "".r | "" match { - | case _: N@@ + | case _: Numb@@ |} |""".stripMargin, """|Number: Regex - |Nil scala.collection.immutable - |NoManifest scala.reflect |""".stripMargin, - topLines = Option(3) + topLines = Some(1) ) - @Test def adt5 = + @Test def `no-methods-on-case-type` = check( s"""|object Main { | val Number = "".r | "" match { - | case _: N@@ + | case _: NotImpl@@ |} |""".stripMargin, - """|Number: Regex - |Nil scala.collection.immutable - |NoManifest scala.reflect + """|NotImplementedError scala |""".stripMargin, - topLines = Option(3) ) @Test def underscore = @@ -1326,6 +1318,171 @@ class CompletionSuite extends BaseCompletionSuite: """|AClass[A <: Int] test.O |AClass test.O |AbstractTypeClassManifest - scala.reflect.ClassManifestFactory + """.stripMargin + ) + + @Test def `extension-definition-scope` = + check( + """|trait Foo + |object T: + | extension (x: Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-symbol-search` = + check( + """|object T: + | extension (x: ListBuffe@@) + |""".stripMargin, + """|ListBuffer[A] - scala.collection.mutable + |ListBuffer - scala.collection.mutable + |""".stripMargin, + ) + + @Test def `extension-definition-type-parameter` = + check( + """|trait Foo + |object T: + | extension [A <: Fo@@] + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-type-parameter-symbol-search` = + check( + """|object T: + | extension [A <: ListBuffe@@] + |""".stripMargin, + """|ListBuffer[A] - scala.collection.mutable + |ListBuffer - scala.collection.mutable + |""".stripMargin + ) + + @Test def `extension-definition-using-param-clause` = + check( + """|trait Foo + |object T: + | extension (using Fo@@) + |""".stripMargin, + """|Foo test |""".stripMargin ) + + @Test def `extension-definition-mix-1` = + check( + """|trait Foo + |object T: + | extension (x: Int)(using Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-2` = + check( + """|trait Foo + |object T: + | extension (using Fo@@)(x: Int)(using Foo) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-3` = + check( + """|trait Foo + |object T: + | extension (using Foo)(x: Int)(using Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-4` = + check( + """|trait Foo + |object T: + | extension [A](x: Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-5` = + check( + """|trait Foo + |object T: + | extension [A](using Fo@@)(x: Int) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-6` = + check( + """|trait Foo + |object T: + | extension [A](using Foo)(x: Fo@@) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-mix-7` = + check( + """|trait Foo + |object T: + | extension [A](using Foo)(x: Fo@@)(using Foo) + |""".stripMargin, + """|Foo test + |""".stripMargin + ) + + @Test def `extension-definition-select` = + check( + """|object Test: + | class TestSelect() + |object T: + | extension (x: Test.TestSel@@) + |""".stripMargin, + """|TestSelect test.Test + |""".stripMargin + ) + + @Test def `extension-definition-select-mix-1` = + check( + """|object Test: + | class TestSelect() + |object T: + | extension (using Int)(x: Test.TestSel@@) + |""".stripMargin, + """|TestSelect test.Test + |""".stripMargin + ) + + @Test def `extension-definition-select-mix-2` = + check( + """|object Test: + | class TestSelect[T]() + |object T: + | extension [T](x: Test.TestSel@@) + |""".stripMargin, + """|TestSelect[T] test.Test + |TestSelect test.Test + |""".stripMargin + ) + + @Test def `no-square-brackets` = + checkEdit( + """|object O: + | val a = List.appl@@ + |""".stripMargin, + """|object O: + | val a = List.apply($0) + |""".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 500b083137e1..c51b66bd5f2e 100644 --- a/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala +++ b/presentation-compiler/test/dotty/tools/pc/tests/completion/CompletionWorkspaceSuite.scala @@ -293,7 +293,6 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: |""".stripMargin ) - // Ignore for Scala 3, since we don't provide completions for null @Test def `match-typed` = checkEdit( """|object Main { @@ -305,11 +304,12 @@ class CompletionWorkspaceSuite extends BaseCompletionSuite: """|import java.util.ArrayDeque |object Main { | def foo(): Unit = null match { - | case x: ArrayDeque => + | case x: ArrayDeque[$0] => | } |} |""".stripMargin, - filter = _.contains("java.util") + filter = _.contains("java.util"), + assertSingleItem = false, ) @Test def `type` =