diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala index aef1b57b89e..8cfa3cb2fd0 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/CompilerSearchVisitor.scala @@ -10,6 +10,7 @@ import scala.meta.internal.metals.ReportContext import scala.meta.pc.* import dotty.tools.dotc.core.Contexts.* +import dotty.tools.dotc.core.Flags import dotty.tools.dotc.core.Names.* import dotty.tools.dotc.core.Symbols.* @@ -20,8 +21,15 @@ class CompilerSearchVisitor( val logger: Logger = Logger.getLogger(classOf[CompilerSearchVisitor].getName) + private def isAccessibleImplicitClass(sym: Symbol) = + val owner = sym.maybeOwner + owner != NoSymbol && owner.isClass && + owner.is(Flags.Implicit) && + owner.isStatic && owner.isPublic + private def isAccessible(sym: Symbol): Boolean = try - sym != NoSymbol && sym.isPublic && sym.isStatic + sym != NoSymbol && sym.isPublic && sym.isStatic || + isAccessibleImplicitClass(sym) catch case err: AssertionError => logger.log(Level.WARNING, err.getMessage()) diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/SemanticdbSymbols.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/SemanticdbSymbols.scala index c8749645d91..3c8bcfa732b 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/SemanticdbSymbols.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/SemanticdbSymbols.scala @@ -48,7 +48,16 @@ object SemanticdbSymbols: // however in scalac this method is defined only in `module Files` if typeSym.is(JavaDefined) then typeSym :: owner.info.decl(termName(value)).symbol :: Nil + /** + * Looks like decl doesn't work for: + * package a: + * implicit class <> (i: Int): + * def inc = i + 1 + */ + else if typeSym == NoSymbol then + owner.info.member(typeName(value)).symbol :: Nil else typeSym :: Nil + end if case Descriptor.Term(value) => val outSymbol = owner.info.decl(termName(value)).symbol if outSymbol.exists @@ -91,6 +100,8 @@ object SemanticdbSymbols: .map(_.symbol) .filter(sym => symbolName(sym) == s) .toList + end match + end tryMember parentSymbol.flatMap(tryMember) try diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala index ea5c2c2ec1a..67da964a58c 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionProvider.scala @@ -224,7 +224,7 @@ class CompletionProvider( def mkItemWithImports( v: CompletionValue.Workspace | CompletionValue.Extension | - CompletionValue.Interpolator + CompletionValue.Interpolator | CompletionValue.ImplicitClass ) = val sym = v.symbol path match @@ -277,7 +277,8 @@ class CompletionProvider( end mkItemWithImports completion match - case v: (CompletionValue.Workspace | CompletionValue.Extension) => + case v: (CompletionValue.Workspace | CompletionValue.Extension | + CompletionValue.ImplicitClass) => mkItemWithImports(v) case v: CompletionValue.Interpolator if v.isWorkspace || v.isExtension => mkItemWithImports(v) diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionValue.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionValue.scala index 8f02ebd3bd0..03173154ef9 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionValue.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/CompletionValue.scala @@ -130,6 +130,20 @@ object CompletionValue: override def description(printer: MetalsPrinter)(using Context): String = s"${printer.completionSymbol(symbol)} (extension)" + /** + * CompletionValue for old implicit classes methods via SymbolSearch + */ + case class ImplicitClass( + label: String, + symbol: Symbol, + override val snippetSuffix: CompletionSuffix, + override val importSymbol: Symbol, + ) extends Symbolic: + override def completionItemKind(using Context): CompletionItemKind = + CompletionItemKind.Method + override def description(printer: MetalsPrinter)(using Context): String = + s"${printer.completionSymbol(symbol)} (implicit)" + /** * @param shortenedNames shortened type names by `Printer`. This field should be used for autoImports * @param start Starting position of the completion diff --git a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala index 7d0b823b5c9..d51d8307bd2 100644 --- a/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala +++ b/mtags/src/main/scala-3/scala/meta/internal/pc/completions/Completions.scala @@ -595,14 +595,40 @@ class Completions( Some(search.search(query, buildTargetIdentifier, visitor)) case CompletionKind.Members => val visitor = new CompilerSearchVisitor(sym => - if sym.is(ExtensionMethod) && + def isExtensionMethod = sym.is(ExtensionMethod) && qualType.widenDealias <:< sym.extensionParam.info.widenDealias - then + + 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) && + 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)) diff --git a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala index c6cafdbb439..46c4ba40a72 100644 --- a/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala +++ b/mtags/src/main/scala/scala/meta/internal/mtags/ScalaToplevelMtags.scala @@ -79,8 +79,16 @@ class ScalaToplevelMtags( expectTemplate: Option[ExpectTemplate], prevWasDot: Boolean = false ): Unit = { - def newExpectTemplate: Some[ExpectTemplate] = - Some(ExpectTemplate(indent, currentOwner, false, false)) + def newExpectTemplate(isImplicit: Boolean = false): Some[ExpectTemplate] = + Some( + ExpectTemplate( + indent, + currentOwner, + false, + false, + isImplicit = isImplicit + ) + ) def newExpectCaseClassTemplate: Some[ExpectTemplate] = Some( ExpectTemplate( @@ -92,20 +100,27 @@ class ScalaToplevelMtags( isCaseClassConstructor = true ) ) - def newExpectClassTemplate: Some[ExpectTemplate] = + def newExpectClassTemplate( + isImplicit: Boolean = false + ): Some[ExpectTemplate] = Some( ExpectTemplate( indent, currentOwner, false, false, - isClassConstructor = true + isClassConstructor = true, + isImplicit = isImplicit ) ) def newExpectPkgTemplate: Some[ExpectTemplate] = Some(ExpectTemplate(indent, currentOwner, true, false)) def newExpectExtensionTemplate(owner: String): Some[ExpectTemplate] = Some(ExpectTemplate(indent, owner, false, true)) + def newExpectImplicitTemplate: Some[ExpectTemplate] = + Some( + ExpectTemplate(indent, currentOwner, false, false, isImplicit = true) + ) def newExpectIgnoreBody: Some[ExpectTemplate] = Some( ExpectTemplate( @@ -130,6 +145,8 @@ class ScalaToplevelMtags( def needEmitTermMember(): Boolean = includeMembers && !prevWasDot + def srcName = input.filename.stripSuffix(".scala") + if (!isDone) { val data = scanner.curr val currRegion = @@ -147,7 +164,7 @@ class ScalaToplevelMtags( val nextRegion = new Region.Package(currentOwner, currRegion) loop(indent, false, nextRegion, newExpectPkgTemplate) } else - loop(indent, false, currRegion, newExpectTemplate) + loop(indent, false, currRegion, newExpectTemplate()) case IDENTIFIER if dialect.allowExtensionMethods && data.name == "extension" => val nextOwner = @@ -177,21 +194,51 @@ class ScalaToplevelMtags( newExpectExtensionTemplate(nextOwner) ) case CLASS | TRAIT | OBJECT | ENUM if needEmitMember(currRegion) => - emitMember(false, currRegion.owner) + /* Scala 3 allows for toplevel implicit classes, but generates + * artificial package object. Scala 2 doesn't allow for it. + */ + val needsToGenerateFileClass = + dialect.allowExtensionMethods && currRegion.produceSourceToplevel && + expectTemplate.exists(_.isImplicit) + val owner = if (needsToGenerateFileClass) { + val name = s"$srcName$$package" + val pos = newPosition + val owner = withOwner(currRegion.owner) { + term(name, pos, Kind.OBJECT, 0) + } + owner + } else if (expectTemplate.exists(_.isImplicit)) { + currRegion.termOwner + } else { currRegion.owner } + emitMember(isPackageObject = false, owner) val template = expectTemplate match { case Some(expect) if expect.isCaseClassConstructor => newExpectCaseClassTemplate - case _ => newExpectClassTemplate + case Some(expect) => + newExpectClassTemplate(expect.isImplicit) + case _ => + newExpectClassTemplate(isImplicit = false) } - loop(indent, isAfterNewline = false, currRegion, template) + loop( + indent, + isAfterNewline = false, + if (needsToGenerateFileClass) currRegion.withTermOwner(owner) + else currRegion, + template + ) // also covers extension methods because of `def` inside case DEF // extension group - if (includeMembers && dialect.allowExtensionMethods && currRegion.isExtension) => + if (includeMembers && (dialect.allowExtensionMethods && currRegion.isExtension || currRegion.isImplicit)) => acceptTrivia() newIdentifier.foreach { name => withOwner(currRegion.owner) { - term(name.name, name.pos, Kind.METHOD, EXTENSION) + method( + name.name, + region.overloads.disambiguator(name.name), + name.pos, + EXTENSION + ) } } loop(indent, isAfterNewline = false, currRegion, newExpectIgnoreBody) @@ -209,7 +256,12 @@ class ScalaToplevelMtags( acceptTrivia() newIdentifier.foreach { name => withOwner(expect.owner) { - term(name.name, name.pos, Kind.METHOD, EXTENSION) + method( + name.name, + region.overloads.disambiguator(name.name), + name.pos, + EXTENSION + ) } } loop(indent, isAfterNewline = false, currRegion, None) @@ -218,7 +270,6 @@ class ScalaToplevelMtags( if dialect.allowToplevelStatements && needEmitFileOwner(currRegion) => val pos = newPosition - val srcName = input.filename.stripSuffix(".scala") val name = s"$srcName$$package" val owner = withOwner(currRegion.owner) { term(name, pos, Kind.OBJECT, 0) @@ -306,7 +357,10 @@ class ScalaToplevelMtags( case COLON if dialect.allowSignificantIndentation => (expectTemplate, nextIsNL()) match { case (Some(expect), true) if needToParseBody(expect) => - val next = expect.startIndentedRegion(currRegion) + val next = expect.startIndentedRegion( + currRegion, + isImplicitClass = expect.isImplicit + ) resetRegion(next) scanner.nextToken() loop(0, isAfterNewline = true, next, None) @@ -340,7 +394,11 @@ class ScalaToplevelMtags( case Some(expect) if needToParseBody(expect) || needToParseExtension(expect) => val next = - expect.startInBraceRegion(currRegion, expect.isExtension) + expect.startInBraceRegion( + currRegion, + expect.isExtension, + expect.isImplicit + ) resetRegion(next) scanner.nextToken() loop(indent, isAfterNewline = false, next, None) @@ -351,7 +409,7 @@ class ScalaToplevelMtags( } case RBRACE => val nextRegion = currRegion match { - case Region.InBrace(_, prev, _, _) => resetRegion(prev) + case Region.InBrace(_, prev, _, _, _) => resetRegion(prev) case r => r } scanner.nextToken() @@ -391,7 +449,10 @@ class ScalaToplevelMtags( indent, isAfterNewline = false, currRegion.prev, - newExpectTemplate + newExpectTemplate( + // we still need the information if the current template is implicit + expectTemplate.exists(_.isImplicit) + ) ) case COMMA => val nextExpectTemplate = expectTemplate.filter(!_.isPackageBody) @@ -422,7 +483,7 @@ class ScalaToplevelMtags( val (shouldCreateClassTemplate, isAfterNewline) = emitEnumCases(region, nextIsNewLine) val nextExpectTemplate = - if (shouldCreateClassTemplate) newExpectClassTemplate + if (shouldCreateClassTemplate) newExpectClassTemplate() else expectTemplate.filter(!_.isPackageBody) loop( indent, @@ -431,6 +492,15 @@ class ScalaToplevelMtags( if (scanner.curr.token == CLASS) newExpectCaseClassTemplate else nextExpectTemplate ) + case IMPLICIT => + scanner.nextToken() + loop( + indent, + isAfterNewline, + currRegion, + newExpectImplicitTemplate, + prevWasDot + ) case t => val nextExpectTemplate = expectTemplate.filter(!_.isPackageBody) scanner.nextToken() @@ -832,7 +902,8 @@ object ScalaToplevelMtags { isExtension: Boolean = false, ignoreBody: Boolean = false, isCaseClassConstructor: Boolean = false, - isClassConstructor: Boolean = false + isClassConstructor: Boolean = false, + isImplicit: Boolean = false ) { /** @@ -845,15 +916,29 @@ object ScalaToplevelMtags { private def adjustRegion(r: Region): Region = if (isPackageBody) r.prev else r - def startInBraceRegion(prev: Region, extension: Boolean = false): Region = - new Region.InBrace(owner, adjustRegion(prev), extension) + def startInBraceRegion( + prev: Region, + extension: Boolean = false, + isImplicitClass: Boolean = false + ): Region = + new Region.InBrace(owner, adjustRegion(prev), extension, isImplicitClass) def startInParenRegion(prev: Region, isCaseClass: Boolean): Region = if (isCaseClass) Region.InParenCaseClass(owner, adjustRegion(prev), true) else Region.InParenClass(owner, adjustRegion(prev)) - def startIndentedRegion(prev: Region, extension: Boolean = false): Region = - new Region.Indented(owner, indent, adjustRegion(prev), extension) + def startIndentedRegion( + prev: Region, + extension: Boolean = false, + isImplicitClass: Boolean = false + ): Region = + new Region.Indented( + owner, + indent, + adjustRegion(prev), + extension, + isImplicitClass: Boolean + ) } @@ -863,6 +948,7 @@ object ScalaToplevelMtags { def acceptMembers: Boolean def produceSourceToplevel: Boolean = termOwner.isPackage def isExtension: Boolean = false + def isImplicit: Boolean = false val overloads: OverloadDisambiguator = new OverloadDisambiguator() def termOwner: String = owner // toplevel terms are wrapped into an artificial Object @@ -898,39 +984,43 @@ object ScalaToplevelMtags { owner: String, prev: Region, extension: Boolean = false, - override val termOwner: String + override val termOwner: String, + override val isImplicit: Boolean ) extends Region { def this( owner: String, prev: Region, - extension: Boolean - ) = this(owner, prev, extension, owner) + extension: Boolean, + isImplicit: Boolean + ) = this(owner, prev, extension, owner, isImplicit) def acceptMembers: Boolean = owner.endsWith("/") override def isExtension = extension override val withTermOwner: String => InBrace = termOwner => - InBrace(owner, prev, extension, termOwner) + InBrace(owner, prev, extension, termOwner, isImplicit) } final case class Indented( owner: String, exitIndent: Int, prev: Region, extension: Boolean = false, - override val termOwner: String + override val termOwner: String, + override val isImplicit: Boolean ) extends Region { def this( owner: String, exitIndent: Int, prev: Region, - extension: Boolean - ) = this(owner, exitIndent, prev, extension, owner) + extension: Boolean, + isImplicit: Boolean + ) = this(owner, exitIndent, prev, extension, owner, isImplicit) def acceptMembers: Boolean = owner.endsWith("/") override def isExtension = extension override val withTermOwner: String => Indented = termOwner => - Indented(owner, exitIndent, prev, extension, termOwner) + Indented(owner, exitIndent, prev, extension, termOwner, isImplicit) } final case class InParenClass( diff --git a/tests/cross/src/main/scala/tests/BasePCSuite.scala b/tests/cross/src/main/scala/tests/BasePCSuite.scala index 84874519f08..f7ef495f650 100644 --- a/tests/cross/src/main/scala/tests/BasePCSuite.scala +++ b/tests/cross/src/main/scala/tests/BasePCSuite.scala @@ -225,6 +225,7 @@ abstract class BasePCSuite extends BaseSuite with PCSuite { } object IgnoreScalaVersion { + def apply(version: String): IgnoreScalaVersion = { IgnoreScalaVersion(_ == version) } diff --git a/tests/cross/src/test/scala/tests/pc/CompletionExtensionMethodSuite.scala b/tests/cross/src/test/scala/tests/pc/CompletionExtensionMethodSuite.scala index c75c9b433fb..e1413bf9430 100644 --- a/tests/cross/src/test/scala/tests/pc/CompletionExtensionMethodSuite.scala +++ b/tests/cross/src/test/scala/tests/pc/CompletionExtensionMethodSuite.scala @@ -21,6 +21,20 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { |""".stripMargin ) + check( + "simple-old-syntax".tag(IgnoreForScala3CompilerPC), + """|package example + | + |object Test: + | implicit class TestOps(a: Int): + | def testOps(b: Int): String = ??? + | + |def main = 100.test@@ + |""".stripMargin, + """|testOps(b: Int): String (implicit) + |""".stripMargin + ) + check( "simple2", """|package example @@ -36,6 +50,21 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { filter = _.contains("(extension)") ) + check( + "simple2-old-syntax".tag(IgnoreForScala3CompilerPC), + """|package example + | + |object enrichments: + | implicit class TestOps(a: Int): + | def testOps(b: Int): String = ??? + | + |def main = 100.t@@ + |""".stripMargin, + """|testOps(b: Int): String (implicit) + |""".stripMargin, + filter = _.contains("(implicit)") + ) + check( "simple-empty", """|package example @@ -51,6 +80,21 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { filter = _.contains("(extension)") ) + check( + "simple-empty-old".tag(IgnoreForScala3CompilerPC), + """|package example + | + |object enrichments: + | implicit class TestOps(a: Int): + | def testOps(b: Int): String = ??? + | + |def main = 100.@@ + |""".stripMargin, + """|testOps(b: Int): String (implicit) + |""".stripMargin, + filter = _.contains("(implicit)") + ) + check( "filter-by-type", """|package example @@ -68,6 +112,23 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { filter = _.contains("(extension)") ) + check( + "filter-by-type-old".tag(IgnoreForScala3CompilerPC), + """|package example + | + |object enrichments: + | implicit class A(num: Int): + | def identity2: Int = num + 1 + | implicit class B(str: String): + | def identity: String = str + | + |def main = "foo".iden@@ + |""".stripMargin, + """|identity: String (implicit) + |""".stripMargin // identity2 won't be available + + ) + check( "filter-by-type-subtype", """|package example @@ -86,6 +147,24 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { filter = _.contains("(extension)") ) + check( + "filter-by-type-subtype-old".tag(IgnoreForScala3CompilerPC), + """|package example + | + |class A + |class B extends A + | + |object enrichments: + | implicit class Test(a: A): + | def doSomething: A = a + | + |def main = (new B).do@@ + |""".stripMargin, + """|doSomething: A (implicit) + |""".stripMargin, + filter = _.contains("(implicit)") + ) + checkEdit( "simple-edit", """|package example @@ -108,6 +187,28 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { |""".stripMargin ) + checkEdit( + "simple-edit-old".tag(IgnoreForScala3CompilerPC), + """|package example + | + |object enrichments: + | implicit class A (num: Int): + | def incr: Int = num + 1 + | + |def main = 100.inc@@ + |""".stripMargin, + """|package example + | + |import example.enrichments.A + | + |object enrichments: + | implicit class A (num: Int): + | def incr: Int = num + 1 + | + |def main = 100.incr + |""".stripMargin + ) + checkEdit( "simple-edit-suffix", """|package example @@ -157,6 +258,28 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { | val plus = 100.plus(19) | val y = 19.plus($0) |} + """.stripMargin + ) + + checkEdit( + "simple-edit-suffix-old".tag(IgnoreForScala3CompilerPC), + """|package example + | + |object enrichments: + | implicit class A (val num: Int): + | def plus(other: Int): Int = num + other + | + |def main = 100.pl@@ + |""".stripMargin, + """|package example + | + |import example.enrichments.A + | + |object enrichments: + | implicit class A (val num: Int): + | def plus(other: Int): Int = num + other + | + |def main = 100.plus($0) |""".stripMargin ) @@ -176,6 +299,23 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { |""".stripMargin ) + check( + "directly-in-pkg1-old" + .tag( + IgnoreScalaVersion.forLessThan("3.2.2").and(IgnoreForScala3CompilerPC) + ), + """| + |package examples: + | implicit class A(num: Int): + | def incr: Int = num + 1 + | + |package examples2: + | def main = 100.inc@@ + |""".stripMargin, + """|incr: Int (implicit) + |""".stripMargin + ) + check( "directly-in-pkg2".tag(IgnoreScalaVersion.forLessThan("3.2.2")), """|package example: @@ -190,6 +330,23 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { |""".stripMargin ) + check( + "directly-in-pkg2-old" + .tag( + IgnoreScalaVersion.forLessThan("3.2.2").and(IgnoreForScala3CompilerPC) + ), + """|package examples: + | object X: + | def fooBar(num: Int) = num + 1 + | implicit class A (num: Int) { def incr: Int = num + 1 } + | + |package examples2: + | def main = 100.inc@@ + |""".stripMargin, + """|incr: Int (implicit) + |""".stripMargin + ) + checkEdit( "directly-in-pkg3".tag(IgnoreScalaVersion.forLessThan("3.2.2")), """|package example: @@ -207,6 +364,26 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { |""".stripMargin ) + checkEdit( + "directly-in-pkg3-old" + .tag( + IgnoreScalaVersion.forLessThan("3.2.2").and(IgnoreForScala3CompilerPC) + ), + """|package examples: + | implicit class A (num: Int) { def incr: Int = num + 1 } + | + |package examples2: + | def main = 100.inc@@ + |""".stripMargin, + """|import examples.A + |package examples: + | implicit class A (num: Int) { def incr: Int = num + 1 } + | + |package examples2: + | def main = 100.incr + |""".stripMargin + ) + check( "nested-pkg".tag(IgnoreScalaVersion.forLessThan("3.2.2")), """|package a: // some comment @@ -225,4 +402,25 @@ class CompletionExtensionMethodSuite extends BaseCompletionSuite { |""".stripMargin ) + check( + "nested-pkg-old" + .tag( + IgnoreScalaVersion.forLessThan("3.2.2").and(IgnoreForScala3CompilerPC) + ), + """|package aa: // some comment + | package cc: + | implicit class A (num: Int): + | def increment2 = num + 2 + | implicit class A (num: Int): + | def increment = num + 1 + | + | + |package bb: + | def main: Unit = 123.incre@@ + |""".stripMargin, + """|increment: Int (implicit) + |increment2: Int (implicit) + |""".stripMargin + ) + } diff --git a/tests/input/src/main/scala-3/example/ToplevelImplicit.scala b/tests/input/src/main/scala-3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..7651f8aee7b --- /dev/null +++ b/tests/input/src/main/scala-3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +package example + +// This wasn't possible in Scala 2 +implicit class Xtension(number: Int) { + def increment: Int = number + 1 +} + +implicit class XtensionAnyVal(private val number: Int) extends AnyVal { + def double: Int = number * 2 +} diff --git a/tests/mtest/src/main/scala/tests/PCSuite.scala b/tests/mtest/src/main/scala/tests/PCSuite.scala index 79f40b5b226..d74e0ec1d9e 100644 --- a/tests/mtest/src/main/scala/tests/PCSuite.scala +++ b/tests/mtest/src/main/scala/tests/PCSuite.scala @@ -100,7 +100,7 @@ trait PCSuite { case NonFatal(e) => println(s"warn: ${e.getMessage}") } - workspace.inputs(filename) = (code2, dialect) + workspace.inputs(file.toURI.toString()) = (code2, dialect) } } diff --git a/tests/slow/src/test/scala/tests/feature/CompletionCrossLspSuite.scala b/tests/slow/src/test/scala/tests/feature/CompletionCrossLspSuite.scala index d2a14f6bb1f..991aa42f4ec 100644 --- a/tests/slow/src/test/scala/tests/feature/CompletionCrossLspSuite.scala +++ b/tests/slow/src/test/scala/tests/feature/CompletionCrossLspSuite.scala @@ -106,6 +106,54 @@ class CompletionCrossLspSuite } yield () } + test("implicit-class") { + cleanWorkspace() + for { + _ <- initialize( + s"""/metals.json + |{ + | "a": { "scalaVersion": "${V.scala3}" } + |} + |/a/src/main/scala/a/B.scala + |package b + |implicit class B (num: Int): + | def plus(other: Int) = num + other + |/a/src/main/scala/a/A.scala + |package a + | + |object A { + | // @@ + |} + |""".stripMargin + ) + _ <- server.didOpen("a/src/main/scala/a/B.scala") + _ = assertNoDiagnostics() + _ <- assertCompletionEdit( + "1.p@@", + """|package a + | + |import b.B + | + |object A { + | 1.plus($0) + |} + |""".stripMargin, + filter = _.contains("plus"), + ) + _ <- assertCompletion( + "1.pl@@", + """|plus(other: Int): Int (implicit) + |""".stripMargin, + filter = _.contains("plus"), + ) + _ <- assertCompletion( + "\"plus is not available for string\".plu@@", + "", + filter = _.contains("plus"), + ) + } yield () + } + test("basic-scala3") { cleanWorkspace() for { diff --git a/tests/unit/src/test/resources/decorations3/example/EndMarker.scala b/tests/unit/src/test/resources/decorations3/example/EndMarker.scala index 26c56797083..cb7489c8a8b 100644 --- a/tests/unit/src/test/resources/decorations3/example/EndMarker.scala +++ b/tests/unit/src/test/resources/decorations3/example/EndMarker.scala @@ -3,4 +3,4 @@ package example object EndMarker: def foo/*: Int*/ = 1 - end foo + end foo \ No newline at end of file diff --git a/tests/unit/src/test/resources/decorations3/example/ToplevelImplicit.scala b/tests/unit/src/test/resources/decorations3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..d9de637fac5 --- /dev/null +++ b/tests/unit/src/test/resources/decorations3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +package example + +// This wasn't possible in Scala 2 +implicit class Xtension(number: Int) { + def increment: Int = number + 1 +} + +implicit class XtensionAnyVal(private val number: Int) extends AnyVal { + def double: Int = number * 2 +} \ No newline at end of file diff --git a/tests/unit/src/test/resources/definition-scala3/example/ToplevelImplicit.scala b/tests/unit/src/test/resources/definition-scala3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..a98666153ea --- /dev/null +++ b/tests/unit/src/test/resources/definition-scala3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +package example + +// This wasn't possible in Scala 2 +implicit class Xtension/**/(number/**/: Int/*Int.scala*/) { + def increment/**/: Int/*Int.scala*/ = number/**/ +/*Int.scala*/ 1 +} + +implicit class XtensionAnyVal/**/(private val number/**/: Int/*Int.scala*/) extends AnyVal/*AnyVal.scala*/ { + def double/**/: Int/*Int.scala*/ = number/**/ */*Int.scala*/ 2 +} diff --git a/tests/unit/src/test/resources/documentSymbol-scala3/example/ToplevelImplicit.scala b/tests/unit/src/test/resources/documentSymbol-scala3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..19cfa235c8a --- /dev/null +++ b/tests/unit/src/test/resources/documentSymbol-scala3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +/*example(Package):10*/package example + +// This wasn't possible in Scala 2 +/*example.Xtension(Class):6*/implicit class Xtension(number: Int) { + /*example.Xtension#increment(Method):5*/def increment: Int = number + 1 +} + +/*example.XtensionAnyVal(Class):10*/implicit class XtensionAnyVal(private val number: Int) extends AnyVal { + /*example.XtensionAnyVal#double(Method):9*/def double: Int = number * 2 +} diff --git a/tests/unit/src/test/resources/expect/toplevels-scala3.expect b/tests/unit/src/test/resources/expect/toplevels-scala3.expect index 39a344acf53..2d139454969 100644 --- a/tests/unit/src/test/resources/expect/toplevels-scala3.expect +++ b/tests/unit/src/test/resources/expect/toplevels-scala3.expect @@ -41,6 +41,9 @@ example/PatternMatching.scala -> example/PatternMatching# example/Scalalib.scala -> example/Scalalib# example/StructuralTypes.scala -> example/StructuralTypes. example/ToplevelDefVal.scala -> example/ToplevelDefVal$package. +example/ToplevelImplicit.scala -> example/ToplevelImplicit$package. +example/ToplevelImplicit.scala -> example/ToplevelImplicit$package.Xtension# +example/ToplevelImplicit.scala -> example/ToplevelImplicit$package.XtensionAnyVal# example/TryCatch.scala -> example/TryCatch# example/TypeEnum.scala -> example/TypeEnum# example/TypeParameters.scala -> example/TypeParameters# @@ -53,4 +56,4 @@ example/nested/LocalClass.scala -> example/nested/LocalClass# example/nested/package.scala -> example/PackageObjectSibling# example/nested/package.scala -> example/nested/package. example/package.scala -> example/package. -example/type/Backtick.scala -> example/type/Backtick# +example/type/Backtick.scala -> example/type/Backtick# \ No newline at end of file diff --git a/tests/unit/src/test/resources/mtags-scala3/example/ToplevelImplicit.scala b/tests/unit/src/test/resources/mtags-scala3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..0df96f53532 --- /dev/null +++ b/tests/unit/src/test/resources/mtags-scala3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +package example + +// This wasn't possible in Scala 2 +implicit class Xtension/*example.Xtension().*//*example.Xtension#*/(number/*example.Xtension#number.*/: Int) { + def increment/*example.Xtension#increment().*/: Int = number + 1 +} + +implicit class XtensionAnyVal/*example.XtensionAnyVal().*//*example.XtensionAnyVal#*/(private val number/*example.XtensionAnyVal#number.*/: Int) extends AnyVal { + def double/*example.XtensionAnyVal#double().*/: Int = number * 2 +} diff --git a/tests/unit/src/test/resources/semanticTokens3/example/EndMarker.scala b/tests/unit/src/test/resources/semanticTokens3/example/EndMarker.scala index 7e4e14ab074..0c9bc254a5a 100644 --- a/tests/unit/src/test/resources/semanticTokens3/example/EndMarker.scala +++ b/tests/unit/src/test/resources/semanticTokens3/example/EndMarker.scala @@ -3,4 +3,4 @@ <>/*keyword*/ <>/*class*/: <>/*keyword*/ <>/*method,definition*/ = <<1>>/*number*/ - <>/*keyword*/ <>/*method,definition*/ + <>/*keyword*/ <>/*method,definition*/ \ No newline at end of file diff --git a/tests/unit/src/test/resources/semanticTokens3/example/ToplevelImplicit.scala b/tests/unit/src/test/resources/semanticTokens3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..ce84a8602c7 --- /dev/null +++ b/tests/unit/src/test/resources/semanticTokens3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +<>/*keyword*/ <>/*namespace*/ + +<>/*comment*/ +<>/*modifier*/ <>/*keyword*/ <>/*class*/(<>/*variable,declaration,readonly*/: <>/*class,abstract*/) { + <>/*keyword*/ <>/*method,definition*/: <>/*class,abstract*/ = <>/*variable,readonly*/ <<+>>/*method*/ <<1>>/*number*/ +} + +<>/*modifier*/ <>/*keyword*/ <>/*class*/(<>/*modifier*/ <>/*keyword*/ <>/*variable,declaration,readonly*/: <>/*class,abstract*/) <>/*keyword*/ <>/*class,abstract*/ { + <>/*keyword*/ <>/*method,definition*/: <>/*class,abstract*/ = <>/*variable,readonly*/ <<*>>/*method*/ <<2>>/*number*/ +} \ No newline at end of file diff --git a/tests/unit/src/test/resources/semanticdb-scala3/example/ToplevelImplicit.scala b/tests/unit/src/test/resources/semanticdb-scala3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..180e4a65ddf --- /dev/null +++ b/tests/unit/src/test/resources/semanticdb-scala3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +package example + +// This wasn't possible in Scala 2 +implicit class Xtension/*example.ToplevelImplicit$package.Xtension#*/(number/*example.ToplevelImplicit$package.Xtension#number.*/: Int/*scala.Int#*/) { + def increment/*example.ToplevelImplicit$package.Xtension#increment().*/: Int/*scala.Int#*/ = number/*example.ToplevelImplicit$package.Xtension#number.*/ +/*scala.Int#`+`(+4).*/ 1 +} + +implicit class XtensionAnyVal/*example.ToplevelImplicit$package.XtensionAnyVal#*/(private val number/*example.ToplevelImplicit$package.XtensionAnyVal#number.*/: Int/*scala.Int#*/) extends AnyVal/*scala.AnyVal#*/ { + def double/*example.ToplevelImplicit$package.XtensionAnyVal#double().*/: Int/*scala.Int#*/ = number/*example.ToplevelImplicit$package.XtensionAnyVal#number.*/ */*scala.Int#`*`(+3).*/ 2 +} diff --git a/tests/unit/src/test/resources/toplevel-with-inner-scala3/example/ToplevelImplicit.scala b/tests/unit/src/test/resources/toplevel-with-inner-scala3/example/ToplevelImplicit.scala new file mode 100644 index 00000000000..2a6ab582f9f --- /dev/null +++ b/tests/unit/src/test/resources/toplevel-with-inner-scala3/example/ToplevelImplicit.scala @@ -0,0 +1,10 @@ +package example + +// This wasn't possible in Scala 2 +implicit class/*example.ToplevelImplicit$package.*/ Xtension/*example.ToplevelImplicit$package.Xtension#*/(number: Int) { + def increment: Int = number + 1 +} + +implicit class XtensionAnyVal/*example.ToplevelImplicit$package.XtensionAnyVal#*/(private val number: Int) extends AnyVal { + def double: Int = number * 2 +} diff --git a/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala b/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala index ae9116ea5b6..169f1de83fc 100644 --- a/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala +++ b/tests/unit/src/test/scala/tests/ScalaToplevelSuite.scala @@ -348,8 +348,8 @@ class ScalaToplevelSuite extends BaseSuite { List( "a/", "a/A.", - "a/A.bar.", - "a/A.foo.", + "a/A.bar().", + "a/A.foo().", ), mode = All, dialect = dialects.Scala3, @@ -369,8 +369,8 @@ class ScalaToplevelSuite extends BaseSuite { List( "a/", "a/Test$package.", - "a/Test$package.bar.", - "a/Test$package.foo.", + "a/Test$package.bar().", + "a/Test$package.foo().", ), mode = All, dialect = dialects.Scala3, @@ -386,8 +386,8 @@ class ScalaToplevelSuite extends BaseSuite { | def baz: Long = ??? |""".stripMargin, List( - "a/", "a/Test$package.", "a/Test$package.foo.", "a/Test$package.bar.", - "a/Test$package.baz.", + "a/", "a/Test$package.", "a/Test$package.foo().", "a/Test$package.bar().", + "a/Test$package.baz().", ), mode = All, dialect = dialects.Scala3,