diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala index 1e63c30db56..399641a3749 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsEnrichments.scala @@ -1,5 +1,6 @@ package scala.meta.internal.metals +import java.io.File import java.io.IOException import java.net.URI import java.nio.charset.StandardCharsets @@ -294,6 +295,16 @@ object MetalsEnrichments implicit class XtensionAbsolutePathBuffers(path: AbsolutePath) { + def hasScalaFiles: Boolean = { + def isScalaDir(file: File): Boolean = { + file.listFiles().exists { file => + if (file.isDirectory()) isScalaDir(file) + else file.getName().endsWith(".scala") + } + } + path.isDirectory && isScalaDir(path.toFile) + } + def scalaSourcerootOption: String = s""""-P:semanticdb:sourceroot:$path"""" def javaSourcerootOption: String = diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala index 1905ff76a94..715beab2f8d 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -881,17 +881,20 @@ class MetalsLspService( def connectTables(): Connection = tables.connect() def initialized(): Future[Unit] = - Future - .sequence( - List[Future[Unit]]( - Future(buildTools.initialize()), - quickConnectToBuildServer().ignoreValue, - slowConnectToBuildServer(forceImport = false).ignoreValue, - Future(workspaceSymbols.indexClasspath()), - Future(formattingProvider.load()), - ) - ) - .ignoreValue + for { + _ <- maybeSetupScalaCli() + _ <- + Future + .sequence( + List[Future[Unit]]( + Future(buildTools.initialize()), + quickConnectToBuildServer().ignoreValue, + slowConnectToBuildServer(forceImport = false).ignoreValue, + Future(workspaceSymbols.indexClasspath()), + Future(formattingProvider.load()), + ) + ) + } yield () def onShutdown(): Unit = { tables.fingerprints.save(fingerprints.getAllFingerprints()) @@ -1904,7 +1907,16 @@ class MetalsLspService( tables.buildServers.chooseServer(ScalaCliBuildTool.name) quickConnectToBuildServer() case Some(digest) if isBloopOrEmpty => - slowConnectToBloopServer(forceImport, buildTool, digest) + for { + _ <- + if (scalaCli.loaded(folder)) scalaCli.stop() + else Future.successful(()) + buildChange <- slowConnectToBloopServer( + forceImport, + buildTool, + digest, + ) + } yield buildChange case Some(digest) => indexer.reloadWorkspaceAndIndex( forceImport, @@ -1918,6 +1930,20 @@ class MetalsLspService( } } yield buildChange + /** + * If there is no auto-connectable build server and no supported build tool is found + * we assume it's a scala-cli project. + */ + def maybeSetupScalaCli(): Future[Unit] = { + if ( + !buildTools.isAutoConnectable + && buildTools.loadSupported.isEmpty + && folder.hasScalaFiles + ) { + scalaCli.setupIDE(folder) + } else Future.successful(()) + } + private def slowConnectToBloopServer( forceImport: Boolean, buildTool: BuildTool, diff --git a/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala b/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala index 6f1b4c69a3b..f323f2357b9 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala @@ -152,7 +152,7 @@ class ScalaCli( } } - private lazy val baseCommand = { + private lazy val localScalaCli: Option[Seq[String]] = { def endsWithCaseInsensitive(s: String, suffix: String): Boolean = s.length >= suffix.length && @@ -217,7 +217,7 @@ class ScalaCli( version.exists(ver => minVersion0.compareTo(ver) <= 0) } - val cliCommand = userConfig().scalaCliLauncher + userConfig().scalaCliLauncher .filter(_.trim.nonEmpty) .map(Seq(_)) .orElse { @@ -226,19 +226,25 @@ class ScalaCli( .filter(requireMinVersion(_, ScalaCli.minVersion)) .map(p => Seq(p.toString)) } - .getOrElse { - scribe.warn( - s"scala-cli >= ${ScalaCli.minVersion} not found in PATH, fetching and starting a JVM-based Scala CLI" - ) - val cp = ScalaCli.scalaCliClassPath() - Seq( - ScalaCli.javaCommand, - "-cp", - cp.mkString(File.pathSeparator), - ScalaCli.scalaCliMainClass, - ) - } - cliCommand ++ Seq("bsp") + } + + private lazy val cliCommand = { + localScalaCli.getOrElse { + scribe.warn( + s"scala-cli >= ${ScalaCli.minVersion} not found in PATH, fetching and starting a JVM-based Scala CLI" + ) + jvmBased() + } + } + + def jvmBased(): Seq[String] = { + val cp = ScalaCli.scalaCliClassPath() + Seq( + ScalaCli.javaCommand, + "-cp", + cp.mkString(File.pathSeparator), + ScalaCli.scalaCliMainClass, + ) } def loaded(path: AbsolutePath): Boolean = @@ -246,6 +252,31 @@ class ScalaCli( st.path == path || path.toNIO.startsWith(st.path.toNIO) )(false) + def setupIDE(path: AbsolutePath): Future[Unit] = { + localScalaCli + .map { cliCommand => + val command = cliCommand ++ Seq("setup-ide", path.toString()) + scribe.info(s"Running $command") + val proc = SystemProcess.run( + command.toList, + path, + redirectErrorOutput = false, + env = Map(), + processOut = None, + processErr = Some(line => scribe.info("Scala CLI: " + line)), + discardInput = false, + threadNamePrefix = "scala-cli-setup-ide", + ) + proc.complete.ignoreValue + } + .getOrElse { + start(path) + } + } + + def path: Option[AbsolutePath] = + ifConnectedOrElse(st => Option(st.path))(None) + def start(path: AbsolutePath): Future[Unit] = { disconnectOldBuildServer().onComplete { case Failure(e) => @@ -253,7 +284,7 @@ class ScalaCli( case Success(()) => } - val command = baseCommand :+ path.toString() + val command = cliCommand :+ "bsp" :+ path.toString() val connDir = if (path.isDirectory) path else path.parent @@ -283,7 +314,7 @@ class ScalaCli( scribe.error("Error starting Scala CLI", ex) Success(()) case Success(_) => - scribe.info("Scala CLI started") + scribe.info(s"Scala CLI started for $path") Success(()) } } else { diff --git a/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala b/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala index 9e096616768..502bf96fc9b 100644 --- a/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala +++ b/tests/slow/src/test/scala/tests/feature/SyntaxErrorLspSuite.scala @@ -161,7 +161,7 @@ class SyntaxErrorLspSuite extends BaseLspSuite("syntax-error") { } yield () } - test("no-build-tool") { + test("no-build-tool") { // we fallback to scala-cli for { _ <- initialize( """ @@ -173,7 +173,10 @@ class SyntaxErrorLspSuite extends BaseLspSuite("syntax-error") { _ <- server.didOpen("A.scala") _ = assertNoDiff( client.workspaceDiagnostics, - """|A.scala:1:20: error: illegal start of simple expression + """|A.scala:1:20: error: expression expected but '}' found + |object A { val x = } + | ^ + |A.scala:1:20: error: illegal start of simple expression |object A { val x = } | ^ |""".stripMargin, diff --git a/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala b/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala index 1645a719741..64c3ae912d4 100644 --- a/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala +++ b/tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala @@ -184,6 +184,23 @@ class ScalaCliSuite extends BaseScalaCliSuite(V.scala3) { } yield () } + + test("connecting-scalacli-as-fallback") { + cleanWorkspace() + FileLayout.fromString(simpleFileLayout, workspace) + for { + _ <- server.initialize() + _ <- server.initialized() + _ <- server.server.indexingPromise.future + _ <- server.didOpen("MyTests.scala") + _ <- assertDefinitionAtLocation( + "MyTests.scala", + "new Fo@@o", + "foo.sc", + ) + } yield () + } + test("relative-semanticdb-root") { for { _ <- scalaCliInitialize(useBsp = false)(