diff --git a/docs/new-editor.md b/docs/new-editor.md index 80e03827922..46988e44ada 100644 --- a/docs/new-editor.md +++ b/docs/new-editor.md @@ -75,6 +75,9 @@ patterns [here](https://github.com/scalameta/scalameta/blob/master/semanticdb/semanticdb3/semanticdb3.md) to learn more about SemanticDB. These files are required for goto definition, find references, hover and Scalafix to work. +* `project/target/active.json`: an indicator of the running sbt server. + Watching this file allows metals to (re)connect to the sbt server whenever it + is (re)started. See the VS Code plugin [clientOptions](https://github.com/scalameta/metals/blob/fb166f1d81eb77ebd9c6b3ee95e65fb58a907eec/vscode-extension/src/extension.ts#L44-L54) diff --git a/metals/src/main/scala/scala/meta/metals/MetalsServices.scala b/metals/src/main/scala/scala/meta/metals/MetalsServices.scala index 7df3b449990..87e31a6a9ff 100644 --- a/metals/src/main/scala/scala/meta/metals/MetalsServices.scala +++ b/metals/src/main/scala/scala/meta/metals/MetalsServices.scala @@ -137,7 +137,7 @@ class MetalsServices( logger.info(s"Initialized with $cwd, $params") LSPLogger.notifications = Some(client) cancelEffects = effects.map(_.subscribe()) - loadAllRelevantFilesInThisWorkspace() + Workspace.initialize(cwd) { onChangedFile(_)(()) } val commands = WorkspaceCommand.values.map(_.entryName) val capabilities = ServerCapabilities( textDocumentSync = Some( @@ -281,7 +281,7 @@ class MetalsServices( Uri(path), FileChangeType.Created | FileChangeType.Changed ) => - onChangedFile(path.toAbsolutePath) { _ => + onChangedFile(path.toAbsolutePath) { logger.warn(s"Unknown file extension for path $path") } @@ -411,14 +411,22 @@ class MetalsServices( response case SbtConnect => Task { - connectToSbtServer() + SbtServer.readVersion(cwd) match { + case Some(ver) if ver.startsWith("0.") || ver.startsWith("1.0") => + showMessage.warn( + s"sbt v${ver} used in this project doesn't have server functionality. " + + "Upgrade to sbt v1.1+ to enjoy Metals integration with the sbt server." + ) + case _ => + connectToSbtServer() + } Right(Json.Null) } } - private def sbtExec(): Unit = sbtServer.foreach { sbt => + private def sbtExec(command: String): Unit = sbtServer.foreach { sbt => Sbt - .exec(latestConfig().sbt.command)(sbt.client) + .exec(command)(sbt.client) .onErrorRecover { case NonFatal(err) => // TODO(olafur) figure out why this "broken pipe" is not getting @@ -431,9 +439,10 @@ class MetalsServices( } .runAsync } + private def sbtExec(): Unit = sbtExec(latestConfig().sbt.command) private def connectToSbtServer(): Unit = { - sbtServer.foreach(_.runningServer.cancel()) + sbtServer.foreach(_.disconnect()) val services = SbtServer.forwardingServices(client, latestConfig) SbtServer.connect(cwd, services)(s.sbt).foreach { case Left(err) => showMessage.error(err) @@ -451,7 +460,7 @@ class MetalsServices( sbtServer = None showMessage.warn("Disconnected from sbt server") } - sbtExec() // run compile right away. + sbtExec() // run configured command right away } } @@ -465,22 +474,19 @@ class MetalsServices( Cursor(Uri(td.uri), contents, offset) } - private def loadAllRelevantFilesInThisWorkspace(): Unit = { - Workspace.initialize(cwd) { path => - onChangedFile(path)(_ => ()) - } - } - private def onChangedFile( path: AbsolutePath - )(fallback: AbsolutePath => Unit): Unit = { + )(fallback: => Unit): Unit = { logger.info(s"File $path changed") - path.toNIO match { + path.toRelative(cwd) match { case Semanticdbs.File() => fileSystemSemanticdbSubscriber.onNext(path) case CompilerConfig.File() => compilerConfigSubscriber.onNext(path) - case _ => fallback(path) + case SbtServer.ActiveJson() => + connectToSbtServer() + case _ => + fallback } } } diff --git a/metals/src/main/scala/scala/meta/metals/Models.scala b/metals/src/main/scala/scala/meta/metals/Models.scala index 7ee564e89a3..1c6e8a5f5e9 100644 --- a/metals/src/main/scala/scala/meta/metals/Models.scala +++ b/metals/src/main/scala/scala/meta/metals/Models.scala @@ -17,5 +17,5 @@ import org.langmeta.io.AbsolutePath @JsonCodec case class SbtExecParams(commandLine: String) case class MissingActiveJson(path: AbsolutePath) - extends Exception(s"sbt-server 1.1.0 is not running, $path does not exist") + extends Exception(s"sbt-server 1.1+ is not running, $path does not exist") case class SbtServerConnectionError(msg: String) extends Exception(msg) diff --git a/metals/src/main/scala/scala/meta/metals/Semanticdbs.scala b/metals/src/main/scala/scala/meta/metals/Semanticdbs.scala index 0a8a8ae5c39..5ef8e55e6fb 100644 --- a/metals/src/main/scala/scala/meta/metals/Semanticdbs.scala +++ b/metals/src/main/scala/scala/meta/metals/Semanticdbs.scala @@ -12,16 +12,17 @@ import scala.tools.nsc.reporters.StoreReporter import scala.util.control.NonFatal import scala.{meta => m} import com.typesafe.scalalogging.LazyLogging -import java.nio.file.Path import org.langmeta.inputs.Input import org.langmeta.internal.io.PathIO import org.langmeta.internal.semanticdb.schema.Database import org.langmeta.io.AbsolutePath +import org.langmeta.io.RelativePath object Semanticdbs extends LazyLogging { object File { - def unapply(path: Path): Boolean = PathIO.extension(path) == "semanticdb" + def unapply(path: RelativePath): Boolean = + PathIO.extension(path.toNIO) == "semanticdb" } def toSemanticdb( diff --git a/metals/src/main/scala/scala/meta/metals/Workspace.scala b/metals/src/main/scala/scala/meta/metals/Workspace.scala index 8f42b9f3576..4044744f5d1 100644 --- a/metals/src/main/scala/scala/meta/metals/Workspace.scala +++ b/metals/src/main/scala/scala/meta/metals/Workspace.scala @@ -7,13 +7,13 @@ import java.nio.file.SimpleFileVisitor import java.nio.file.attribute.BasicFileAttributes import org.langmeta.internal.io.FileIO import org.langmeta.io.AbsolutePath -import org.langmeta.io.RelativePath import scala.meta.metals.compiler.CompilerConfig +import scala.meta.metals.sbtserver.SbtServer object Workspace { def compilerConfigFiles(cwd: AbsolutePath): Iterable[AbsolutePath] = { - val configDir = cwd.resolve(RelativePath(CompilerConfig.Directory)) + val configDir = CompilerConfig.dir(cwd) if (configDir.isDirectory) { FileIO.listAllFilesRecursively(configDir) } else { @@ -22,9 +22,10 @@ object Workspace { } def initialize(cwd: AbsolutePath)( - callback: AbsolutePath => Unit + action: AbsolutePath => Unit ): Unit = { - compilerConfigFiles(cwd).foreach(callback) + compilerConfigFiles(cwd).foreach(action) + Files.walkFileTree( cwd.toNIO, new SimpleFileVisitor[Path] { @@ -32,14 +33,16 @@ object Workspace { file: Path, attrs: BasicFileAttributes ): FileVisitResult = { - file match { - case Semanticdbs.File() => - callback(AbsolutePath(file)) + val absPath = AbsolutePath(file) + absPath.toRelative(cwd) match { + case Semanticdbs.File() => action(absPath) case _ => // ignore, to avoid spamming console. } FileVisitResult.CONTINUE } } ) + + action(SbtServer.ActiveJson(cwd)) } } diff --git a/metals/src/main/scala/scala/meta/metals/compiler/CompilerConfig.scala b/metals/src/main/scala/scala/meta/metals/compiler/CompilerConfig.scala index 6f87d54d763..7acb06c1863 100644 --- a/metals/src/main/scala/scala/meta/metals/compiler/CompilerConfig.scala +++ b/metals/src/main/scala/scala/meta/metals/compiler/CompilerConfig.scala @@ -6,9 +6,9 @@ import java.util.Properties import scala.tools.nsc.settings.ScalaVersion import scala.tools.nsc.settings.SpecificScalaVersion import com.typesafe.scalalogging.LazyLogging -import java.nio.file.Path import org.langmeta.internal.io.PathIO import org.langmeta.io.AbsolutePath +import org.langmeta.io.RelativePath import scala.util.control.NonFatal /** @@ -58,12 +58,16 @@ case class CompilerConfig( } object CompilerConfig extends LazyLogging { - val Directory: Path = Paths.get(".metals").resolve("buildinfo") + private val relativeDir: RelativePath = + RelativePath(".metals").resolve("buildinfo") + + def dir(cwd: AbsolutePath): AbsolutePath = + cwd.resolve(relativeDir) + object File { - def unapply(path: Path): Boolean = { - Files.exists(path) && - path.getParent.getParent.endsWith(Directory) && - PathIO.extension(path) == "properties" + def unapply(path: RelativePath): Boolean = { + path.toNIO.startsWith(relativeDir.toNIO) && + PathIO.extension(path.toNIO) == "properties" } } diff --git a/metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala b/metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala index fe9e4d79645..1d0e53ba228 100644 --- a/metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala +++ b/metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala @@ -4,6 +4,8 @@ import java.io.IOException import java.net.URI import java.nio.ByteBuffer import java.nio.file.Files +import java.nio.file.Path +import java.util.Properties import scala.meta.metals.ActiveJson import scala.meta.metals.MissingActiveJson import scala.meta.metals.SbtInitializeParams @@ -15,6 +17,7 @@ import monix.eval.Task import monix.execution.CancelableFuture import monix.execution.Scheduler import org.langmeta.io.AbsolutePath +import org.langmeta.io.RelativePath import org.langmeta.jsonrpc.BaseProtocolMessage import org.langmeta.jsonrpc.JsonRpcClient import org.langmeta.jsonrpc.Services @@ -30,7 +33,6 @@ import org.scalasbt.ipcsocket.UnixDomainSocket * @param client client that can send requests and notifications * to the sbt server. * @param runningServer The running client listening for requests from the server. - * Use runningServer.cancel() to stop disconnect to this server. * Use runningServer.onComplete to attach callbacks on * disconnect. * @@ -38,11 +40,33 @@ import org.scalasbt.ipcsocket.UnixDomainSocket case class SbtServer( client: JsonRpcClient, runningServer: CancelableFuture[Unit] -) +) { + def disconnect(): Unit = runningServer.cancel() +} object SbtServer extends LazyLogging { private def fail(message: String) = Task.now(Left(message)) + /** + * Tries to read sbt version from the `project/build.properties` file. + * + * @param cwd sbt project root directory. + * @return version string value or `None` if anything goes wrong. + */ + def readVersion(cwd: AbsolutePath): Option[String] = { + val props = new Properties() + val path = cwd.resolve("project").resolve("build.properties") + if (path.isFile) { + val input = Files.newInputStream(path.toNIO) + try { + props.load(input) + } finally { + input.close() + } + } + Option(props.getProperty("sbt.version")) + } + /** * Establish connection with sbt server. * @@ -113,8 +137,16 @@ object SbtServer extends LazyLogging { /** * Returns path to project/target/active.json from the base directory of an sbt build. */ - def activeJson(cwd: AbsolutePath): AbsolutePath = - cwd.resolve("project").resolve("target").resolve("active.json") + object ActiveJson { + private val relativePath: RelativePath = + RelativePath("project").resolve("target").resolve("active.json") + + def apply(cwd: AbsolutePath): AbsolutePath = + cwd.resolve(relativePath) + + def unapply(path: Path): Boolean = + path.endsWith(relativePath.toNIO) + } /** * Establishes a unix domain socket connection with sbt server. @@ -122,11 +154,11 @@ object SbtServer extends LazyLogging { def openSocketConnection( cwd: AbsolutePath ): Either[Throwable, UnixDomainSocket] = { - val active = activeJson(cwd) + val path = ActiveJson(cwd) for { bytes <- { - if (Files.exists(active.toNIO)) Right(Files.readAllBytes(active.toNIO)) - else Left(MissingActiveJson(active)) + if (path.isFile) Right(Files.readAllBytes(path.toNIO)) + else Left(MissingActiveJson(path)) } parsed <- parseByteBuffer(ByteBuffer.wrap(bytes)) activeJson <- parsed.as[ActiveJson] diff --git a/vscode-extension/src/extension.ts b/vscode-extension/src/extension.ts index 76c24963393..3b521e56437 100644 --- a/vscode-extension/src/extension.ts +++ b/vscode-extension/src/extension.ts @@ -46,7 +46,8 @@ export async function activate(context: ExtensionContext) { synchronize: { fileEvents: [ workspace.createFileSystemWatcher("**/*.semanticdb"), - workspace.createFileSystemWatcher("**/.metals/buildinfo/**/*.properties") + workspace.createFileSystemWatcher("**/.metals/buildinfo/**/*.properties"), + workspace.createFileSystemWatcher("**/project/target/active.json") ], configurationSection: 'metals' },