From de4ad2b6c0bef31ee4a3750572222e95fcd9a278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Rochala?= <48657087+rochala@users.noreply.github.com> Date: Tue, 3 Oct 2023 16:22:17 +0200 Subject: [PATCH] Support completions for extension definition parameter (#18331) Extension methods are extended into normal definitions. Because of that typed trees don't include any information about the extension method definition parameter: ```scala extension (x: In@@) ``` In order to add completions, we check if there is an exact path to the untyped tree, and if not, we fall back to it. There may also be more possible cases like that, but I can't think of any at the moment. --- .../tools/dotc/interactive/Completion.scala | 128 ++++++++---- .../src/dotty/tools/repl/ReplCompiler.scala | 35 +++- .../src/dotty/tools/repl/ReplDriver.scala | 5 +- .../dotty/tools/repl/TabcompleteTests.scala | 5 + .../tools/languageserver/CompletionTest.scala | 71 ++++++- .../tools/pc/completions/Completions.scala | 168 +++++---------- .../pc/printer/ShortenedTypePrinter.scala | 15 +- .../pc/tests/completion/CompletionSuite.scala | 197 ++++++++++++++++-- .../completion/CompletionWorkspaceSuite.scala | 6 +- 9 files changed, 421 insertions(+), 209 deletions(-) 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` =