Skip to content

Commit

Permalink
Merge pull request #278 from laughedelic/smart-sbt-connection
Browse files Browse the repository at this point in the history
Smart sbt connection
  • Loading branch information
gabro authored Apr 13, 2018
2 parents 905bf28 + d7eb533 commit a3ffb91
Show file tree
Hide file tree
Showing 8 changed files with 90 additions and 40 deletions.
3 changes: 3 additions & 0 deletions docs/new-editor.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
38 changes: 22 additions & 16 deletions metals/src/main/scala/scala/meta/metals/MetalsServices.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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")
}

Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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
}
}

Expand All @@ -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
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion metals/src/main/scala/scala/meta/metals/Models.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
5 changes: 3 additions & 2 deletions metals/src/main/scala/scala/meta/metals/Semanticdbs.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
17 changes: 10 additions & 7 deletions metals/src/main/scala/scala/meta/metals/Workspace.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,24 +22,27 @@ 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] {
override def visitFile(
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))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand Down Expand Up @@ -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"
}
}

Expand Down
46 changes: 39 additions & 7 deletions metals/src/main/scala/scala/meta/metals/sbtserver/SbtServer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -30,19 +33,40 @@ 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.
*
*/
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.
*
Expand Down Expand Up @@ -113,20 +137,28 @@ 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.
*/
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]
Expand Down
3 changes: 2 additions & 1 deletion vscode-extension/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
},
Expand Down

0 comments on commit a3ffb91

Please sign in to comment.