diff --git a/mtags-shared/src/main/scala/scala/meta/internal/mtags/CoursierComplete.scala b/mtags-shared/src/main/scala/scala/meta/internal/mtags/CoursierComplete.scala index 2c3068d0826..7be3f77bf45 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/mtags/CoursierComplete.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/mtags/CoursierComplete.scala @@ -20,35 +20,62 @@ class CoursierComplete(scalaVersion: String) { else scalaVersion.split('.').take(2).mkString(".") ) + private def completions(s: String): List[String] = { + val futureCompletions = Future { + api.withInput(s).complete().getCompletions().asScala.toList + } + try Await.result(futureCompletions, 10.seconds) + catch { + case _: Throwable => Nil + } + } + def complete( dependency: String, - includeScala: Boolean = true + includeScala: Boolean = true, + supportNonJvm: Boolean = true ): List[String] = { + // Version completions + if (dependency.replaceAll(":+", ":").count(_ == ':') == 2) { + versionCompletions(dependency, supportNonJvm) + } else { + val javaCompletions = completions(dependency) + val scalaCompletions = + if ( + includeScala && + dependency.endsWith(":") && dependency.count(_ == ':') == 1 + ) + completions(dependency + ":").map(":" + _) + else List.empty - def completions(s: String): List[String] = { - val futureCompletions = Future { - api.withInput(s).complete().getCompletions().asScala.toList - } - try Await.result(futureCompletions, 10.seconds) - catch { - case _: Throwable => Nil - } + (scalaCompletions ++ javaCompletions).distinct } + } + + private def versionCompletions( + dependency: String, + supportNonJvm: Boolean + ): List[String] = { + val (adjusted, hasDoubleColon) = adjustDoubleColon(dependency) + val sortedCompletions = completions(adjusted).sortWith( + Version.fromString(_) >= Version.fromString(_) + ) + if (!hasDoubleColon && supportNonJvm) + sortedCompletions.map(":" + _) + else sortedCompletions + + } + + private def adjustDoubleColon(dependency: String): (String, Boolean) = { + val doubleColon = dependency.lastIndexOf("::") + val firstColon = dependency.indexOf(":") + + if (doubleColon > firstColon) { + val depString = (dependency.substring(0, doubleColon) + + dependency.substring(doubleColon + 1, dependency.length())) + (depString, true) + } else (dependency, false) - val javaCompletions = completions(dependency) - val scalaCompletions = - if ( - includeScala && - dependency.endsWith(":") && dependency.count(_ == ':') == 1 - ) - completions(dependency + ":").map(":" + _) - else List.empty - - val allCompletions = (scalaCompletions ++ javaCompletions).distinct - // Attempt to sort versions in reverse order - if (dependency.replaceAll(":+", ":").count(_ == ':') == 2) - allCompletions.sortWith(Version.fromString(_) >= Version.fromString(_)) - else allCompletions } } diff --git a/mtags-shared/src/main/scala/scala/meta/internal/semver/SemVer.scala b/mtags-shared/src/main/scala/scala/meta/internal/semver/SemVer.scala index 3bce597f9e4..0350ce64067 100644 --- a/mtags-shared/src/main/scala/scala/meta/internal/semver/SemVer.scala +++ b/mtags-shared/src/main/scala/scala/meta/internal/semver/SemVer.scala @@ -49,12 +49,12 @@ object SemVer { def fromString(version: String): Version = { val parts = version.split("\\.|-") val Array(major, minor, patch) = - parts.take(3).map(part => Try { part.toInt }.getOrElse(0)) + parts.take(3).map(tryToInt) val (rc, milestone) = parts .lift(3) .map { v => - if (v.startsWith("RC")) (Some(v.stripPrefix("RC").toInt), None) - else if (v.startsWith("M")) (None, Some(v.stripPrefix("M").toInt)) + if (v.startsWith("RC")) (Some(tryToInt(v.stripPrefix("RC"))), None) + else if (v.startsWith("M")) (None, Some(tryToInt(v.stripPrefix("M")))) else (None, None) } .getOrElse((None, None)) @@ -68,6 +68,8 @@ object SemVer { } + private def tryToInt(s: String): Int = Try { s.toInt }.toOption.getOrElse(0) + def isCompatibleVersion(minimumVersion: String, version: String): Boolean = { Version.fromString(version) >= Version.fromString(minimumVersion) } diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MillIvyCompletions.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MillIvyCompletions.scala index 71dff09c46d..4fe34e28614 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MillIvyCompletions.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/MillIvyCompletions.scala @@ -31,7 +31,10 @@ trait MillIvyCompletions { ) extends DependencyCompletion { override def contribute: List[Member] = { val completions = - coursierComplete.complete(dependency.replace(CURSOR, "")) + coursierComplete.complete( + dependency.replace(CURSOR, ""), + supportNonJvm = false + ) val (editStart, editEnd) = CoursierComplete.inferEditRange(pos.point, text) val editRange = pos.withStart(editStart).withEnd(editEnd).toLsp diff --git a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/SbtLibCompletions.scala b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/SbtLibCompletions.scala index 6e9e5445ac9..ac5ae2d3fe4 100644 --- a/mtags/src/main/scala-2/scala/meta/internal/pc/completions/SbtLibCompletions.scala +++ b/mtags/src/main/scala-2/scala/meta/internal/pc/completions/SbtLibCompletions.scala @@ -67,7 +67,8 @@ trait SbtLibCompletions { val completions = coursierComplete.complete( dependency.replace(CURSOR, ""), - includeScala = false + includeScala = false, + supportNonJvm = false ) val editRange = pos.withStart(pos.start + 1).withEnd(pos.end - 1 - cursorLen).toLsp diff --git a/tests/cross/src/test/scala/tests/pc/CompletionMillIvySuite.scala b/tests/cross/src/test/scala/tests/pc/CompletionMillIvySuite.scala index 1280038efdd..71f69031e4f 100644 --- a/tests/cross/src/test/scala/tests/pc/CompletionMillIvySuite.scala +++ b/tests/cross/src/test/scala/tests/pc/CompletionMillIvySuite.scala @@ -27,6 +27,16 @@ class CompletionMillIvySuite extends BaseCompletionSuite { filename = "build.sc", ) + checkEdit( + "scala-completions-edit", + """|val dependency = ivy"io.circe:@@" + |""".stripMargin, + """|val dependency = ivy"io.circe::circe-config" + |""".stripMargin, + filename = "build.sc", + filter = _ == "circe-config", + ) + check( "scala-completions", """|val dependency = ivy"io.circe::circe-core@@" @@ -48,4 +58,51 @@ class CompletionMillIvySuite extends BaseCompletionSuite { |""".stripMargin, filename = "build.sc", ) + + check( + "version-double-colon", + """|val dependency = ivy"org.typelevel:cats-core_2.11::@@" + |""".stripMargin, + """|1.0.1 + |1.0.0 + |1.0.0-RC2 + |1.0.0-RC1 + |1.0.0-MF + |""".stripMargin, + filter = _.startsWith("1.0"), + filename = "build.sc", + ) + + checkEdit( + "version-no-double-colon-edit", + """|val dependency = ivy"org.typelevel:cats-core_2.11:@@" + |""".stripMargin, + """|val dependency = ivy"org.typelevel:cats-core_2.11:1.0.1" + |""".stripMargin, + filter = _ == "1.0.1", + filename = "build.sc", + ) + + check( + "version-double-colon2", + """|val dependency = ivy"org.typelevel:cats-core_2.11::1.0.@@" + |""".stripMargin, + """|1.0.1 + |1.0.0 + |1.0.0-RC2 + |1.0.0-RC1 + |1.0.0-MF + |""".stripMargin, + filename = "build.sc", + ) + + checkEdit( + "version-double-colon-edit", + """|val dependency = ivy"org.typelevel:cats-core_2.11::1.0.1@@" + |""".stripMargin, + """|val dependency = ivy"org.typelevel:cats-core_2.11::1.0.1" + |""".stripMargin, + filename = "build.sc", + ) + } diff --git a/tests/cross/src/test/scala/tests/pc/CompletionScalaCliSuite.scala b/tests/cross/src/test/scala/tests/pc/CompletionScalaCliSuite.scala index bf96d12fbf2..71a0867df26 100644 --- a/tests/cross/src/test/scala/tests/pc/CompletionScalaCliSuite.scala +++ b/tests/cross/src/test/scala/tests/pc/CompletionScalaCliSuite.scala @@ -46,6 +46,16 @@ class CompletionScalaCliSuite extends BaseCompletionSuite { "0.14.1", ) + checkEdit( + "version-edit", + """|//> using lib "io.circe::circe-core_sjs1:0.14.1@@" + |package A + |""".stripMargin, + """|//> using lib "io.circe::circe-core_sjs1::0.14.1" + |package A + |""".stripMargin, + ) + check( "multiple-libs", """|//> using lib "io.circe::circe-core:0.14.0", "io.circe::circe-core_na@@" @@ -110,7 +120,7 @@ class CompletionScalaCliSuite extends BaseCompletionSuite { """|//> using lib "co.fs2::fs2-core:@@" |package A |""".stripMargin, - """|//> using lib "co.fs2::fs2-core:3.4.0" + """|//> using lib "co.fs2::fs2-core::3.4.0" |package A |""".stripMargin, filter = _.startsWith("3.4"), @@ -144,6 +154,49 @@ class CompletionScalaCliSuite extends BaseCompletionSuite { |""".stripMargin, ) + check( + "version-double-colon", + """|//> using lib "com.outr::scribe-cats::@@" + |package A + |""".stripMargin, + """|3.7.1 + |3.7.0 + |""".stripMargin, + filter = _.startsWith("3.7"), + ) + + checkEdit( + "version-double-colon-edit", + """|//> using lib "com.outr::scribe-cats::@@" + |package A + |""".stripMargin, + """|//> using lib "com.outr::scribe-cats::3.7.1" + |package A + |""".stripMargin, + filter = _.startsWith("3.7.1"), + ) + + check( + "version-double-colon2", + """|//> using lib "com.outr::scribe-cats::3.7@@" + |package A + |""".stripMargin, + """|3.7.1 + |3.7.0 + |""".stripMargin, + ) + + checkEdit( + "version-double-colon-edit2", + """|//> using lib "com.outr::scribe-cats::3.7@@" + |package A + |""".stripMargin, + """|//> using lib "com.outr::scribe-cats::3.7.1" + |package A + |""".stripMargin, + filter = _.startsWith("3.7.1"), + ) + private def scriptWrapper(code: String, filename: String): String = // Vaguely looks like a scala file that ScalaCLI generates // from a sc file. diff --git a/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala b/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala index 23c0e2d6227..e1115282931 100644 --- a/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala +++ b/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala @@ -16,6 +16,7 @@ class ScalaCliActionsSuite val newestOsLib: String = coursierComplete .complete("com.lihaoyi::os-lib:") .headOption + .map(_.stripPrefix(":")) .getOrElse("0.8.1") checkScalaCLI(