diff --git a/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala b/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala index f302d057b2d..b7114e8799c 100644 --- a/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala +++ b/metals/src/main/scala/scala/meta/internal/bsp/BspServers.scala @@ -4,6 +4,7 @@ import java.nio.charset.Charset import java.nio.charset.StandardCharsets import java.security.MessageDigest +import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.Future import scala.concurrent.Promise @@ -19,6 +20,7 @@ import scala.meta.internal.metals.Directories import scala.meta.internal.metals.JdkSources import scala.meta.internal.metals.MetalsBuildClient import scala.meta.internal.metals.MetalsEnrichments._ +import scala.meta.internal.metals.MetalsProjectDirectories import scala.meta.internal.metals.MetalsServerConfig import scala.meta.internal.metals.QuietInputStream import scala.meta.internal.metals.SocketConnection @@ -33,7 +35,6 @@ import scala.meta.io.AbsolutePath import ch.epfl.scala.bsp4j.BspConnectionDetails import com.google.gson.Gson -import dev.dirs.ProjectDirectories /** * Implements BSP server discovery, named "BSP Connection Protocol" in the spec. @@ -202,11 +203,18 @@ final class BspServers( } object BspServers { - def globalInstallDirectories: List[AbsolutePath] = { - val dirs = ProjectDirectories.fromPath("bsp") - List(dirs.dataLocalDir, dirs.dataDir).distinct - .map(path => Try(AbsolutePath(path)).toOption) - .flatten + def globalInstallDirectories(implicit + ec: ExecutionContext + ): List[AbsolutePath] = { + val dirs = MetalsProjectDirectories.fromPath("bsp") + dirs match { + case Some(dirs) => + List(dirs.dataLocalDir, dirs.dataDir).distinct + .map(path => Try(AbsolutePath(path)).toOption) + .flatten + case None => + Nil + } } def readInBspConfig( diff --git a/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala b/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala index 21587e6cc43..5040ae79650 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/BloopServers.scala @@ -15,6 +15,7 @@ import java.util.concurrent.atomic.AtomicInteger import scala.annotation.tailrec import scala.collection.concurrent.TrieMap +import scala.concurrent.ExecutionContext import scala.concurrent.ExecutionContextExecutorService import scala.concurrent.Future import scala.concurrent.Promise @@ -36,7 +37,6 @@ import bloop.rifle.BloopRifleConfig import bloop.rifle.BloopRifleLogger import bloop.rifle.BspConnection import bloop.rifle.BspConnectionAddress -import dev.dirs.ProjectDirectories /** * Establishes a connection with a bloop server using Bloop Launcher. @@ -61,11 +61,11 @@ final class BloopServers( import BloopServers._ - private def metalsJavaHome = - sys.props - .get("java.home") - .orElse(sys.env.get("JAVA_HOME")) - + private def metalsJavaHome = sys.props + .get("java.home") + .orElse(sys.env.get("JAVA_HOME")) + private val bloopWorkingDir = createBloopWorkingDir + private val bloopDaemonDir = bloopWorkingDir.resolve("daemon") private val folderIdMap = TrieMap.empty[AbsolutePath, Int] def shutdownServer(): Boolean = { @@ -112,7 +112,7 @@ final class BloopServers( .recover { case NonFatal(e) => Try( // Bloop output - BloopServers.bloopDaemonDir.resolve("output").readText + bloopDaemonDir.resolve("output").readText ).foreach { scribe.error(_) } @@ -434,19 +434,29 @@ object BloopServers { // Needed for creating unique socket files for each bloop connection private[BloopServers] val connectionCounter = new AtomicInteger(0) - private val bloopDirectories = { - // Scala CLI is still used since we wanted to avoid having two separate servers - ProjectDirectories.from(null, null, "ScalaCli") - } - - lazy val bloopDaemonDir: AbsolutePath = - bloopWorkingDir.resolve("daemon") - - lazy val bloopWorkingDir: AbsolutePath = { - val baseDir = - if (Properties.isMac) bloopDirectories.cacheDir - else bloopDirectories.dataLocalDir - AbsolutePath(Paths.get(baseDir).resolve("bloop")) + def createBloopWorkingDir(implicit ec: ExecutionContext): AbsolutePath = { + + val baseDir = MetalsProjectDirectories.from(null, null, "ScalaCli") match { + case None => + val userHome = Paths.get(System.getProperty("user.home")) + + val potential = + if (Properties.isWin) userHome.resolve("AppData/Local/ScalaCli/data") + else if (Properties.isMac) userHome.resolve("Library/Caches/ScalaCli") + else userHome.resolve(".local/share/scalacli") + Files.createDirectories(potential) + if (potential.toFile.exists()) potential + else + throw new IllegalStateException( + s"Could not create directory $potential for Bloop, please try using a different BSP server and reporting your issue." + ) + case Some(bloopDirectories) => + val baseDir = + if (Properties.isMac) bloopDirectories.cacheDir + else bloopDirectories.dataLocalDir + Paths.get(baseDir) + } + AbsolutePath(baseDir.resolve("bloop")) } def fetchBloop(version: String): Either[Throwable, Seq[File]] = { diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsProjectDirectories.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsProjectDirectories.scala new file mode 100644 index 00000000000..15b85ce28a1 --- /dev/null +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsProjectDirectories.scala @@ -0,0 +1,43 @@ +package scala.meta.internal.metals + +import scala.concurrent.Await +import scala.concurrent.ExecutionContext +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.util.Failure +import scala.util.Success +import scala.util.Try + +import dev.dirs.ProjectDirectories + +object MetalsProjectDirectories { + + def from(qualifier: String, organization: String, application: String)( + implicit ec: ExecutionContext + ): Option[ProjectDirectories] = + wrap { () => + ProjectDirectories.from(qualifier, organization, application) + } + + def fromPath(path: String)(implicit + ec: ExecutionContext + ): Option[ProjectDirectories] = + wrap { () => + ProjectDirectories.fromPath(path) + } + + private def wrap( + f: () => ProjectDirectories + )(implicit ec: ExecutionContext): Option[ProjectDirectories] = { + Try { + val dirs = Future { f() } + Await.result(dirs, 10.seconds) + + } match { + case Failure(exception) => + scribe.error("Failed to get project directories", exception) + None + case Success(value) => Some(value) + } + } +} diff --git a/metals/src/main/scala/scala/meta/internal/metals/MetalsServerInputs.scala b/metals/src/main/scala/scala/meta/internal/metals/MetalsServerInputs.scala index 50adbbc106c..9b38b8dadef 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/MetalsServerInputs.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/MetalsServerInputs.scala @@ -3,6 +3,8 @@ package scala.meta.internal.metals import java.nio.charset.Charset import java.nio.charset.StandardCharsets +import scala.concurrent.ExecutionContext + import scala.meta.internal.bsp.BspServers import scala.meta.internal.metals.Buffers import scala.meta.internal.metals.ClasspathSearch @@ -69,7 +71,8 @@ object MetalsServerInputs { time = Time.system, initialServerConfig = MetalsServerConfig.default, initialUserConfig = UserConfiguration.default, - bspGlobalDirectories = BspServers.globalInstallDirectories, + bspGlobalDirectories = + BspServers.globalInstallDirectories(ExecutionContext.global), mtagsResolver = MtagsResolver.default(), onStartCompilation = () => (), redirectSystemOut = true, diff --git a/metals/src/main/scala/scala/meta/internal/metals/Trace.scala b/metals/src/main/scala/scala/meta/internal/metals/Trace.scala index 6349be00e3e..a34a5c608c7 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/Trace.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/Trace.scala @@ -6,33 +6,34 @@ import java.nio.file.Paths import java.nio.file.StandardOpenOption import scala.annotation.tailrec +import scala.concurrent.ExecutionContext import scala.util.Try import scala.meta.internal.io.PathIO import scala.meta.io.AbsolutePath -import dev.dirs.ProjectDirectories - /** * Manages JSON-RPC tracing of incoming/outgoing messages via BSP and LSP. */ object Trace { // jvm-directories can fail for less common OS versions: https://github.com/soc/directories-jvm/issues/17 - val globalDirectory: Option[AbsolutePath] = + def globalDirectory(implicit ec: ExecutionContext): Option[AbsolutePath] = Try { val projectDirectories = - ProjectDirectories.from("org", "scalameta", "metals") + MetalsProjectDirectories.from("org", "scalameta", "metals") // NOTE(olafur): strictly speaking we should use `dataDir` instead of `cacheDir` but on // macOS this maps to `$HOME/Library/Application Support` which has an annoying space in // the path making it difficult to tail/cat from the terminal and cmd+click from VS Code. // Instead, we use the `cacheDir` which has no spaces. The logs are safe to remove so // putting them in the "cache directory" makes more sense compared to the "config directory". - val cacheDir = Paths.get(projectDirectories.cacheDir) - // https://github.com/scalameta/metals/issues/5590 - // deal with issue on windows and PowerShell, which would cause us to create a null directory in the workspace - if (cacheDir.isAbsolute()) - Some(AbsolutePath(cacheDir)) - else None + projectDirectories.flatMap { dirs => + val cacheDir = Paths.get(dirs.cacheDir) + // https://github.com/scalameta/metals/issues/5590 + // deal with issue on windows and PowerShell, which would cause us to create a null directory in the workspace + if (cacheDir.isAbsolute()) + Some(AbsolutePath(cacheDir)) + else None + } }.toOption.flatten private val localDirectory: AbsolutePath = @@ -44,7 +45,7 @@ object Trace { // normally be fine, but in others like nvim where "workspace" doesn't really // exist, we can only rely on the rootUri that is passed in, but we don't have // access to that yet when we use this. - val metalsLog: AbsolutePath = + def metalsLog(implicit ec: ExecutionContext): AbsolutePath = globalDirectory.getOrElse(localDirectory).resolve("metals.log") def protocolTracePath( @@ -63,7 +64,7 @@ object Trace { def setupTracePrinter( protocolName: String, workspace: AbsolutePath = PathIO.workingDirectory, - ): Option[PrintWriter] = { + )(implicit ec: ExecutionContext): Option[PrintWriter] = { val metalsDir = workspace.resolve(".metals") val tracePaths = (metalsDir :: globalDirectory.toList).map(dir => protocolTracePath(protocolName, dir) diff --git a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala index f1c4b2a8778..77062207cc8 100644 --- a/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala +++ b/metals/src/main/scala/scala/meta/internal/metals/debug/DebugProxy.scala @@ -354,7 +354,7 @@ private[debug] object DebugProxy { workspace: AbsolutePath, endpoint: RemoteEndpoint, name: String, - ): RemoteEndpoint = + )(implicit ec: ExecutionContext): RemoteEndpoint = Trace.setupTracePrinter(name, workspace) match { case Some(trace) => new EndpointLogger(endpoint, trace) case None => endpoint diff --git a/metals/src/main/scala/scala/meta/metals/Main.scala b/metals/src/main/scala/scala/meta/metals/Main.scala index 4af43c1a8a7..4a92fe99742 100644 --- a/metals/src/main/scala/scala/meta/metals/Main.scala +++ b/metals/src/main/scala/scala/meta/metals/Main.scala @@ -37,10 +37,10 @@ object Main extends SupportedScalaVersions { } val systemIn = System.in val systemOut = System.out - MetalsLogger.redirectSystemOut(Trace.metalsLog) - val tracePrinter = Trace.setupTracePrinter("LSP") val exec = Executors.newCachedThreadPool() val ec = ExecutionContext.fromExecutorService(exec) + MetalsLogger.redirectSystemOut(Trace.metalsLog(ec)) + val tracePrinter = Trace.setupTracePrinter("LSP")(ec) val sh = Executors.newSingleThreadScheduledExecutor() val server = new MetalsLanguageServer(ec, sh) try { diff --git a/project/TestGroups.scala b/project/TestGroups.scala index 74668bc76b2..a624a6e9a6e 100644 --- a/project/TestGroups.scala +++ b/project/TestGroups.scala @@ -123,5 +123,4 @@ object TestGroups { "tests.ServerLivenessMonitorSuite", "tests.ResetWorkspaceLspSuite", "tests.ToplevelWithInnerScala3Suite"), ) - } diff --git a/tests/slow/src/main/scala/tests/scalacli/BaseScalaCLIActionSuite.scala b/tests/slow/src/main/scala/tests/scalacli/BaseScalaCLIActionSuite.scala index b5caa0d2485..0a393090de0 100644 --- a/tests/slow/src/main/scala/tests/scalacli/BaseScalaCLIActionSuite.scala +++ b/tests/slow/src/main/scala/tests/scalacli/BaseScalaCLIActionSuite.scala @@ -66,7 +66,6 @@ class BaseScalaCLIActionSuite(name: String) changeFile: String => String = identity, expectError: Boolean = false, filterAction: CodeAction => Boolean = _ => true, - retryAction: Int = 0, )(implicit loc: Location): Unit = { val path = toPath(fileName) @@ -97,7 +96,6 @@ class BaseScalaCLIActionSuite(name: String) expectError, filterAction, overrideLayout = layout, - retryAction, ) } } diff --git a/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala b/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala index 3926de64f68..c6e306bd8a7 100644 --- a/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala +++ b/tests/slow/src/test/scala/tests/scalacli/ScalaCliActionsSuite.scala @@ -53,8 +53,6 @@ class ScalaCliActionsSuite scalaCliOptions = List("--actions", "-S", scalaVersion), expectNoDiagnostics = false, selectedActionIndex = 1, - // Scala CLI doesn't publish everything with the normal diagnostics, but later - retryAction = 5, ) checkScalaCLI( @@ -85,8 +83,6 @@ class ScalaCliActionsSuite scalaCliOptions = List("--actions", "-S", scalaVersion), expectNoDiagnostics = false, selectedActionIndex = 1, - // Scala CLI doesn't publish everything with the normal diagnostics, but later - retryAction = 5, ) checkNoActionScalaCLI( diff --git a/tests/unit/src/main/scala/tests/Library.scala b/tests/unit/src/main/scala/tests/Library.scala index 2f5daa8b16a..1a8faf215ce 100644 --- a/tests/unit/src/main/scala/tests/Library.scala +++ b/tests/unit/src/main/scala/tests/Library.scala @@ -87,7 +87,7 @@ object Library { val fetch = Fetch .create() .withMainArtifacts() - .withClassifiers(Set("sources", "_").asJava) + .addClassifiers("sources") .withDependencies( deps: _* ) diff --git a/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala b/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala index 078e4c0c384..6350030fa41 100644 --- a/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala +++ b/tests/unit/src/main/scala/tests/codeactions/BaseCodeActionLspSuite.scala @@ -59,7 +59,6 @@ abstract class BaseCodeActionLspSuite( expectError: Boolean = false, filterAction: CodeAction => Boolean = _ => true, overrideLayout: Option[String] = None, - retryAction: Int = 0, )(implicit loc: Location): Unit = { val scalacOptionsJson = if (scalacOptions.nonEmpty) @@ -89,7 +88,6 @@ abstract class BaseCodeActionLspSuite( changeFile, expectError, filterAction, - retryAction, ) } @@ -107,7 +105,6 @@ abstract class BaseCodeActionLspSuite( changeFile: String => String = identity, expectError: Boolean = false, filterAction: CodeAction => Boolean = _ => true, - retryAction: Int = 0, )(implicit loc: Location): Unit = { val files = FileLayout.mapFromString(layout) val (path, input) = files @@ -123,24 +120,6 @@ abstract class BaseCodeActionLspSuite( if (renamePath.nonEmpty) input.replace("<<", "").replace(">>", "") else expectedCode - def assertActionsWithRetry( - retry: Int = retryAction - ): Future[List[CodeAction]] = { - server - .assertCodeAction( - path, - changeFile(input), - expectedActions, - kind, - filterAction = filterAction, - ) - .recoverWith { - case _: Throwable if retry > 0 => - Thread.sleep(1000) - assertActionsWithRetry(retry - 1) - case _: Throwable if expectError => Future.successful(Nil) - } - } test(name) { cleanWorkspace() for { @@ -156,7 +135,18 @@ abstract class BaseCodeActionLspSuite( path, changeFile(input).replace("<<", "").replace(">>", ""), ) - codeActions <- assertActionsWithRetry() + codeActions <- + server + .assertCodeAction( + path, + changeFile(input), + expectedActions, + kind, + filterAction = filterAction, + ) + .recover { + case _: Throwable if expectError => Nil + } _ <- client.applyCodeAction(selectedActionIndex, codeActions, server) _ <- server.didSave(newPath) { _ => if (newPath != path)