Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle workspaces with multiple build files. #1758

Merged
merged 3 commits into from
May 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions metals/src/main/resources/db/migration/V1__Create_tables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,7 @@ create table dismissed_notification(
when_expires timestamp
);

-- The choice of build tool when multiple build tool files are found in a workspace
create table chosen_build_tool(
build_tool varchar primary key
);
20 changes: 13 additions & 7 deletions metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,23 +81,29 @@ final class BuildTools(
if (isBazel) buf += "Bazel"
buf.result()
}

def isEmpty: Boolean = {
all.isEmpty
}
def loadSupported(): Option[BuildTool] = {
if (isSbt) Some(SbtBuildTool(workspace, userConfig))
else if (isGradle) Some(GradleBuildTool(userConfig))
else if (isMaven) Some(MavenBuildTool(userConfig))
else if (isMill) Some(MillBuildTool(userConfig))
else if (isPants) Some(PantsBuildTool(userConfig))
else None

def loadSupported(): List[BuildTool] = {
val buf = List.newBuilder[BuildTool]

if (isSbt) buf += SbtBuildTool(workspace, userConfig)
if (isGradle) buf += GradleBuildTool(userConfig)
if (isMaven) buf += MavenBuildTool(userConfig)
if (isMill) buf += MillBuildTool(userConfig)
if (isPants) buf += PantsBuildTool(userConfig)

buf.result()
}

override def toString: String = {
val names = all.mkString("+")
if (names.isEmpty) "<no build tool>"
else names
}

def isBuildRelated(workspace: AbsolutePath, path: AbsolutePath): Boolean = {
if (isSbt) SbtBuildTool.isSbtRelatedPath(workspace, path)
else if (isGradle) GradleBuildTool.isGradleRelatedPath(workspace, path)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import scala.meta.io.AbsolutePath
import scala.meta.internal.process.ProcessHandler
import scala.meta.internal.process.ExitCodes
import scala.meta.internal.metals.Messages._
import org.eclipse.lsp4j.MessageActionItem

/**
* Runs `sbt/gradle/mill/mvn bloopInstall` processes.
Expand Down Expand Up @@ -166,6 +167,15 @@ final class BloopInstall(
}
}

def checkForChosenBuildTool(
buildTools: List[BuildTool]
): Future[Option[BuildTool]] =
tables.buildTool.selectedBuildTool match {
case Some(chosen) =>
Future(buildTools.find(_.executableName == chosen))
case None => requestBuildToolChoice(buildTools)
}

private def persistChecksumStatus(
status: Status,
buildTool: BuildTool
Expand Down Expand Up @@ -201,4 +211,16 @@ final class BloopInstall(
}
}

private def requestBuildToolChoice(
buildTools: List[BuildTool]
): Future[Option[BuildTool]] = {
languageClient
.showMessageRequest(ChooseBuildTool.params(buildTools))
.asScala
.map { choice =>
buildTools.find(buildTool =>
new MessageActionItem(buildTool.executableName) == choice
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package scala.meta.internal.metals

import java.sql.Connection
import JdbcEnrichments._

class ChosenBuildTool(conn: () => Connection) {
def selectedBuildTool(): Option[String] = {
conn()
.query(
"select * from chosen_build_tool LIMIT 1;"
)(_ => ()) { _.getString("build_tool") }
.headOption
}
def chooseBuildTool(buildTool: String): Int = {
conn().update {
"insert into chosen_build_tool values (?);"
} { stmt => stmt.setString(1, buildTool) }
}
}
19 changes: 17 additions & 2 deletions metals/src/main/scala/scala/meta/internal/metals/Doctor.scala
Original file line number Diff line number Diff line change
Expand Up @@ -248,10 +248,16 @@ final class Doctor(

private def buildTargetsJson(): String = {
val targets = allTargets()
val heading = tables.buildTool.selectedBuildTool() match {
case Some(value) =>
doctorHeading + s"\n\nYour ${value} build definition has been imported."
case None => doctorHeading
}

val results = if (targets.isEmpty) {
DoctorResults(
doctorTitle,
doctorHeading,
heading,
Some(
List(
DoctorMessage(
Expand All @@ -266,7 +272,7 @@ final class Doctor(
} else {
val targetResults =
targets.sortBy(_.baseDirectory).map(extractTargetInfo)
DoctorResults(doctorTitle, doctorHeading, None, Some(targetResults)).toJson
DoctorResults(doctorTitle, heading, None, Some(targetResults)).toJson
}
ujson.write(results)
}
Expand All @@ -276,6 +282,15 @@ final class Doctor(
.element("p")(
_.text(doctorHeading)
)

tables.buildTool.selectedBuildTool() match {
case Some(value) =>
html.element("p")(
_.text(s"Your ${value} build definition has been imported.")
)
case None => ()
}

val targets = allTargets()
if (targets.isEmpty) {
html
Expand Down
13 changes: 13 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/Messages.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,20 @@ object Messages {
)
params
}
}

object ChooseBuildTool {
def params(builtTools: List[BuildTool]): ShowMessageRequestParams = {
val messageActionItems =
builtTools.map(bt => new MessageActionItem(bt.executableName))
val params = new ShowMessageRequestParams()
params.setMessage(
"Multiple build definitions found. Which would you like to use?"
)
params.setType(MessageType.Info)
params.setActions(messageActionItems.asJava)
params
}
}

val PartialNavigation = new MetalsStatusParams(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1444,44 +1444,56 @@ class MetalsLanguageServer(
.orNull
}.asJava

private def supportedBuildTool(): Option[BuildTool] =
buildTools.loadSupported match {
case Some(buildTool) =>
val isCompatibleVersion = SemVer.isCompatibleVersion(
buildTool.minimumVersion,
buildTool.version
private def supportedBuildTool(): Future[Option[BuildTool]] = {
def isCompatibleVersion(buildTool: BuildTool) = {
val isCompatibleVersion = SemVer.isCompatibleVersion(
buildTool.minimumVersion,
buildTool.version
)
if (isCompatibleVersion) {
Some(buildTool)
} else {
scribe.warn(s"Unsupported $buildTool version ${buildTool.version}")
languageClient.showMessage(
Messages.IncompatibleBuildToolVersion.params(buildTool)
)
if (isCompatibleVersion) {
Some(buildTool)
} else {
scribe.warn(s"Unsupported $buildTool version ${buildTool.version}")
languageClient.showMessage(
Messages.IncompatibleBuildToolVersion.params(buildTool)
)
None
}
case None =>
None
}
}

buildTools.loadSupported match {
ckipp01 marked this conversation as resolved.
Show resolved Hide resolved
case Nil => {
if (!buildTools.isAutoConnectable) {
warnings.noBuildTool()
}
None
Future(None)
}
case buildTool :: Nil => Future(isCompatibleVersion(buildTool))
case buildTools =>
for {
Some(buildTool) <- bloopInstall.checkForChosenBuildTool(buildTools)
} yield isCompatibleVersion(buildTool)
}
}

private def slowConnectToBuildServer(
forceImport: Boolean
): Future[BuildChange] = {
supportedBuildTool match {
case Some(buildTool) =>
buildTool.digest(workspace) match {
case None =>
scribe.warn(s"Skipping build import, no checksum.")
Future.successful(BuildChange.None)
case Some(digest) =>
slowConnectToBuildServer(forceImport, buildTool, digest)
}
case None =>
Future.successful(BuildChange.None)
}
for {
possibleBuildTool <- supportedBuildTool
buildChange <- possibleBuildTool match {
case Some(buildTool) =>
buildTool.digest(workspace) match {
case None =>
scribe.warn(s"Skipping build import, no checksum.")
Future.successful(BuildChange.None)
case Some(digest) =>
slowConnectToBuildServer(forceImport, buildTool, digest)
}
case None =>
Future.successful(BuildChange.None)
}
} yield buildChange
}

private def slowConnectToBuildServer(
Expand Down
2 changes: 2 additions & 0 deletions metals/src/main/scala/scala/meta/internal/metals/Tables.scala
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ final class Tables(
new DismissedNotifications(() => connection, time)
val buildServers =
new ChosenBuildServers(() => connection, time)
val buildTool =
new ChosenBuildTool(() => connection)

def connect(): Unit = {
this._connection =
Expand Down
67 changes: 67 additions & 0 deletions tests/slow/src/test/scala/tests/MultipleBuildFilesLspSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package tests

import scala.meta.internal.metals.{BuildInfo => V}
import scala.meta.internal.builds.SbtBuildTool
import scala.meta.io.AbsolutePath
import scala.meta.internal.metals.Messages.ChooseBuildTool
import scala.meta.internal.builds.MillBuildTool

class MultipleBuildFilesLspSuite
extends BaseImportSuite("multiple-build-files") {

// SBT will be the main tool for this test, which is what will be
// chosen when the user is prompted in the test
val buildTool: SbtBuildTool = SbtBuildTool(None, () => userConfig)

val alternativeBuildTool: MillBuildTool = MillBuildTool(() => userConfig)

def chooseBuildToolMessage: String =
ChooseBuildTool.params(List(buildTool, alternativeBuildTool)).getMessage

override def currentDigest(
workspace: AbsolutePath
): Option[String] = None

test("basic") {
cleanWorkspace()
for {
_ <- server.initialize(
s"""|/build.sbt
|scalaVersion := "${V.scala212}"
|/build.sc
|import mill._, scalalib._
|object foo extends ScalaModule {
| def scalaVersion = "${V.scala212}"
|}
|""".stripMargin
)
_ = assertNoDiff(
client.workspaceMessageRequests,
List(
// Project has no .bloop directory so user is asked to "import via bloop"
chooseBuildToolMessage,
importBuildMessage,
progressMessage
).mkString("\n")
)
_ = client.messageRequests.clear() // restart
_ = assertNoDiff(client.workspaceMessageRequests, "")
_ <- server.didChange("build.sbt") { text =>
text + "\nversion := \"1.0.0\"\n"
}
_ = assertNoDiff(client.workspaceMessageRequests, "")
_ <- server.didSave("build.sbt")(identity)
} yield {
assertNoDiff(
client.workspaceMessageRequests,
List(
// Ensure that after a choice was made, the user doesn't get re-prompted
// to choose their build tool again
importBuildChangesMessage,
progressMessage
).mkString("\n")
)
}
}

}
20 changes: 20 additions & 0 deletions tests/unit/src/main/scala/tests/TestingClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import org.eclipse.lsp4j.CodeAction
import org.eclipse.lsp4j.WorkspaceEdit
import org.eclipse.lsp4j.Command
import scala.concurrent.Future
import scala.meta.internal.builds.BuildTool

/**
* Fake LSP client that responds to notifications/requests initiated by the server.
Expand Down Expand Up @@ -227,6 +228,21 @@ final class TestingClient(workspace: AbsolutePath, buffers: Buffers)
.map(tool => createParams(tool.toString()))
.contains(params)
}
// NOTE: (ckipp01) Just for easiness of testing, we are going to just look
// for sbt and mill builds together, which are most common. The logic however
// is identical for all build tools.
def isSameMessageFromList(
createParams: List[BuildTool] => ShowMessageRequestParams
): Boolean = {
val buildTools = BuildTools
.default()
.allAvailable
.filter(bt => bt.executableName == "sbt" || bt.executableName == "mill")

val targetParams = createParams(buildTools)
params == targetParams
}

CompletableFuture.completedFuture {
messageRequests.addLast(params.getMessage)
showMessageRequestHandler(params).getOrElse {
Expand All @@ -240,6 +256,10 @@ final class TestingClient(workspace: AbsolutePath, buffers: Buffers)
CheckDoctor.moreInformation
} else if (SelectBspServer.isSelectBspServer(params)) {
params.getActions.asScala.find(_.getTitle == "Bob").get
} else if (isSameMessageFromList(ChooseBuildTool.params)) {
params.getActions.asScala.toList
.find(_.getTitle == "sbt")
.getOrElse(new MessageActionItem("fail"))
} else if (MissingScalafmtConf.isCreateScalafmtConf(params)) {
null
} else {
Expand Down
15 changes: 15 additions & 0 deletions tests/unit/src/test/scala/tests/ChosenBuildToolSuite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package tests

import scala.meta.internal.metals.ChosenBuildTool

class ChosenBuildToolSuite extends BaseTablesSuite {
def buildTool: ChosenBuildTool = tables.buildTool
test("basic") {
assert(buildTool.selectedBuildTool().isEmpty)
assertDiffEqual(buildTool.chooseBuildTool("sbt"), 1)
assertDiffEqual(
buildTool.selectedBuildTool().get,
"sbt"
)
}
}
Loading