diff --git a/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala b/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala index 620d0b53173..5f2196281f5 100644 --- a/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala +++ b/metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala @@ -121,21 +121,40 @@ final class BuildTools( ) def isBazel: Boolean = bazelProject.isDefined + private def customProjectRoot = + userConfig().customProjectRoot + .map(_.trim() match { + case "." => workspace + case relativePath => workspace.resolve(relativePath) + }) + .filter { projectRoot => + val exists = projectRoot.exists + if (!exists) { + scribe.error(s"custom project root $projectRoot does not exist") + } + exists + } + private def searchForBuildTool( isProjectRoot: AbsolutePath => Boolean - ): Option[AbsolutePath] = - if (isProjectRoot(workspace)) Some(workspace) - else - workspace.toNIO - .toFile() - .listFiles() - .collectFirst { - case file - if file.isDirectory && - !file.getName.startsWith(".") && - isProjectRoot(AbsolutePath(file.toPath())) => - AbsolutePath(file.toPath()) - } + ): Option[AbsolutePath] = { + customProjectRoot match { + case Some(projectRoot) => Some(projectRoot).filter(isProjectRoot) + case None => + if (isProjectRoot(workspace)) Some(workspace) + else + workspace.toNIO + .toFile() + .listFiles() + .collectFirst { + case file + if file.isDirectory && + !file.getName.startsWith(".") && + isProjectRoot(AbsolutePath(file.toPath())) => + AbsolutePath(file.toPath()) + } + } + } def allAvailable: List[BuildTool] = { List( 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 8ba4ebc850d..4c87355b9b2 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala @@ -957,6 +957,13 @@ class MetalsLspService( compilers.restartAll() } + val slowConnect = + if (userConfig.customProjectRoot != old.customProjectRoot) { + tables.buildTool.reset() + tables.buildServers.reset() + slowConnectToBuildServer(false).ignoreValue + } else Future.successful(()) + val resetDecorations = if ( userConfig.showImplicitArguments != old.showImplicitArguments || @@ -1008,11 +1015,10 @@ class MetalsLspService( } .getOrElse(Future.successful(())) - Future - .sequence( - List(restartBuildServer, resetDecorations) - ) - .map(_ => ()) + for { + _ <- slowConnect + _ <- Future.sequence(List(restartBuildServer, resetDecorations)) + } yield () } override def didOpen( diff --git a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala index e9707c42b89..e22ed45bced 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/UserConfiguration.scala @@ -53,6 +53,7 @@ case class UserConfiguration( testUserInterface: TestUserInterfaceKind = TestUserInterfaceKind.CodeLenses, javaFormatConfig: Option[JavaFormatConfig] = None, scalafixRulesDependencies: List[String] = Nil, + customProjectRoot: Option[String] = None, scalaCliLauncher: Option[String] = None, ) { @@ -313,6 +314,14 @@ object UserConfiguration { |launcher, not available in PATH. |""".stripMargin, ), + UserConfigurationOption( + "custom-project-root", + """empty string `""`.""", + """"backend/scalaProject/"""", + "Scala CLI launcher", + """Optional relative path to your project's root. + |If you want your project root to be the workspace/workspace root set it to "." .""".stripMargin, + ), ) def fromJson( @@ -525,6 +534,8 @@ object UserConfiguration { val scalafixRulesDependencies = getStringListKey("scalafix-rules-dependencies").getOrElse(Nil) + val customProjectRoot = getStringKey("custom-project-root") + if (errors.isEmpty) { Right( UserConfiguration( @@ -555,6 +566,7 @@ object UserConfiguration { disableTestCodeLenses, javaFormatConfig, scalafixRulesDependencies, + customProjectRoot, ) ) } else { diff --git a/tests/slow/src/test/scala/tests/CustomProjectRootLspSuite.scala b/tests/slow/src/test/scala/tests/CustomProjectRootLspSuite.scala new file mode 100644 index 00000000000..7bffad312c1 --- /dev/null +++ b/tests/slow/src/test/scala/tests/CustomProjectRootLspSuite.scala @@ -0,0 +1,37 @@ +package tests +import scala.meta.internal.metals.UserConfiguration +import scala.meta.internal.metals.{BuildInfo => V} + +class CustomProjectRootLspSuite + extends BaseLspSuite("custom-project-root", BloopImportInitializer) { + override def userConfig: UserConfiguration = + UserConfiguration().copy(customProjectRoot = Some("inner/inner/")) + + test("basic") { + cleanWorkspace() + for { + _ <- initialize( + s"""|/build.sbt + |scalaVersion := "${V.scala213}" + |/inner/inner/project.scala + |//> using scala ${V.scala3} + |/inner/inner/Main.scala + |package a + | + |val k = 1 + |val m: Int = "aaa" + |""".stripMargin + ) + _ <- server.didOpen("inner/inner/Main.scala") + _ = assert(server.server.bspSession.exists(_.main.isScalaCLI)) + _ = assertEquals( + client.workspaceDiagnostics, + """|inner/inner/Main.scala:4:14: error: Found: ("aaa" : String) + |Required: Int + |val m: Int = "aaa" + | ^^^^^ + |""".stripMargin, + ) + } yield () + } +}