Skip to content

Commit 4b401c9

Browse files
committed
improvement: offer amend to handle single file scala-cli config on new file
1 parent 4ee4a40 commit 4b401c9

File tree

11 files changed

+214
-9
lines changed

11 files changed

+214
-9
lines changed

metals/src/main/scala/scala/meta/internal/bsp/BspConfigGenerator.scala

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import scala.meta.internal.builds.BuildServerProvider
88
import scala.meta.internal.builds.ShellRunner
99
import scala.meta.internal.metals.Messages.BspProvider
1010
import scala.meta.internal.metals.MetalsEnrichments._
11+
import scala.meta.internal.metals.StatusBar
1112
import scala.meta.internal.metals.clients.language.MetalsLanguageClient
1213
import scala.meta.io.AbsolutePath
1314

@@ -20,6 +21,7 @@ final class BspConfigGenerator(
2021
workspace: AbsolutePath,
2122
languageClient: MetalsLanguageClient,
2223
shellRunner: ShellRunner,
24+
statusBar: StatusBar,
2325
)(implicit ec: ExecutionContext) {
2426
def runUnconditionally(
2527
buildTool: BuildServerProvider,
@@ -46,6 +48,7 @@ final class BspConfigGenerator(
4648
status <- buildTool.generateBspConfig(
4749
workspace,
4850
args => runUnconditionally(buildTool, args),
51+
statusBar,
4952
)
5053
} yield (buildTool, status)
5154
}

metals/src/main/scala/scala/meta/internal/bsp/BspConnector.scala

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,9 @@ class BspConnector(
296296
buildTool
297297
.generateBspConfig(
298298
workspace,
299-
args => bspConfigGenerator.runUnconditionally(buildTool, args),
299+
args =>
300+
bspConfigGenerator.runUnconditionally(buildTool, args),
301+
statusBar,
300302
)
301303
.map(status => handleGenerationStatus(buildTool, status))
302304
case Right(details) if details.getName == BloopServers.name =>
@@ -329,6 +331,7 @@ class BspConnector(
329331
.generateBspConfig(
330332
workspace,
331333
args => bspConfigGenerator.runUnconditionally(buildTool, args),
334+
statusBar,
332335
)
333336
.map(status => handleGenerationStatus(buildTool, status))
334337
case Right(connectionDetails) =>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package scala.meta.internal.bsp
2+
3+
import scala.util.Try
4+
5+
import scala.meta.internal.builds.ScalaCliBuildTool
6+
import scala.meta.internal.metals.MetalsEnrichments._
7+
import scala.meta.io.AbsolutePath
8+
9+
object ScalaCliBspScope {
10+
def inScope(root: AbsolutePath, file: AbsolutePath): Boolean = {
11+
val roots = scalaCliBspRoot(root)
12+
roots.isEmpty || roots.exists(bspRoot =>
13+
file.toNIO.startsWith(bspRoot.toNIO)
14+
)
15+
}
16+
17+
private def scalaCliBspRoot(root: AbsolutePath): List[AbsolutePath] =
18+
for {
19+
path <- ScalaCliBuildTool.pathsToScalaCliBsp(root)
20+
text <- path.readTextOpt.toList
21+
json = ujson.read(text)
22+
args <- json("argv").arrOpt.toList
23+
tailArgs = args.toList.flatMap(_.strOpt).dropWhile(_ != "bsp")
24+
rootArg <- tailArgs match {
25+
case "bsp" :: tail => dropOptions(tail).takeWhile(!_.startsWith("-"))
26+
case _ => Nil
27+
}
28+
rootPath <- Try(AbsolutePath(rootArg).dealias).toOption
29+
if rootPath.exists
30+
} yield rootPath
31+
32+
private def dropOptions(args: List[String]): List[String] =
33+
args match {
34+
case s"--$_" :: _ :: tail => dropOptions(tail)
35+
case rest => rest
36+
}
37+
}

metals/src/main/scala/scala/meta/internal/builds/BuildServerProvider.scala

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import scala.concurrent.Future
44

55
import scala.meta.internal.bsp.BspConfigGenerationStatus._
66
import scala.meta.internal.metals.Messages
7+
import scala.meta.internal.metals.StatusBar
78
import scala.meta.io.AbsolutePath
89

910
/**
@@ -18,6 +19,7 @@ trait BuildServerProvider extends BuildTool {
1819
def generateBspConfig(
1920
workspace: AbsolutePath,
2021
systemProcess: List[String] => Future[BspConfigGenerationStatus],
22+
statusBar: StatusBar,
2123
): Future[BspConfigGenerationStatus] =
2224
createBspFileArgs(workspace).map(systemProcess).getOrElse {
2325
Future.successful(

metals/src/main/scala/scala/meta/internal/builds/BuildTools.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ final class BuildTools(
115115
if (isGradle) buf += GradleBuildTool(userConfig)
116116
if (isMaven) buf += MavenBuildTool(userConfig)
117117
if (isMill) buf += MillBuildTool(userConfig)
118-
if (isScalaCli) buf += ScalaCliBuildTool(workspace, userConfig)
118+
if (isScalaCli)
119+
buf += ScalaCliBuildTool(workspace, userConfig)
119120

120121
buf.result()
121122
}

metals/src/main/scala/scala/meta/internal/builds/ScalaCliBuildTool.scala

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import scala.concurrent.Future
77
import scala.meta.internal.bsp.BspConfigGenerationStatus._
88
import scala.meta.internal.metals.BuildInfo
99
import scala.meta.internal.metals.MetalsEnrichments._
10+
import scala.meta.internal.metals.StatusBar
1011
import scala.meta.internal.metals.UserConfiguration
1112
import scala.meta.internal.metals.scalacli.ScalaCli
1213
import scala.meta.io.AbsolutePath
@@ -23,21 +24,25 @@ class ScalaCliBuildTool(
2324
override def generateBspConfig(
2425
workspace: AbsolutePath,
2526
systemProcess: List[String] => Future[BspConfigGenerationStatus],
27+
statusBar: StatusBar,
2628
): Future[BspConfigGenerationStatus] =
2729
createBspFileArgs(workspace).map(systemProcess).getOrElse {
2830
// fallback to creating `.bsp/scala-cli.json` that starts JVM launcher
2931
val bspConfig = workspace.resolve(".bsp").resolve("scala-cli.json")
30-
bspConfig.writeText(ScalaCli.scalaCliBspJsonContent())
32+
statusBar.addMessage("scala-cli bspConfig")
33+
bspConfig
34+
.writeText(ScalaCli.scalaCliBspJsonContent(root = workspace.toString()))
3135
Future.successful(Generated)
3236
}
3337

3438
def createBspConfigIfNone(
3539
workspace: AbsolutePath,
3640
systemProcess: List[String] => Future[BspConfigGenerationStatus],
41+
statusBar: StatusBar,
3742
): Future[BspConfigGenerationStatus] = {
3843
if (ScalaCliBuildTool.pathsToScalaCliBsp(workspace).exists(_.isFile))
3944
Future.successful(Generated)
40-
else generateBspConfig(workspace, systemProcess)
45+
else generateBspConfig(workspace, systemProcess, statusBar)
4146
}
4247

4348
override def createBspFileArgs(
@@ -89,7 +94,10 @@ object ScalaCliBuildTool {
8994
json = ujson.read(text)
9095
version <- json("version").strOpt
9196
} yield version
92-
new ScalaCliBuildTool(workspaceFolderVersions.headOption, userConfig)
97+
new ScalaCliBuildTool(
98+
workspaceFolderVersions.headOption,
99+
userConfig,
100+
)
93101
}
94102
}
95103

metals/src/main/scala/scala/meta/internal/metals/Messages.scala

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package scala.meta.internal.metals
22

3+
import java.nio.file.Path
4+
35
import scala.collection.mutable
46

57
import scala.meta.internal.builds.BuildTool
@@ -1009,3 +1011,24 @@ object Messages {
10091011
}
10101012

10111013
}
1014+
1015+
object FileOutOfScalaCliBspScope {
1016+
val regenerateAndRestart = new MessageActionItem("Yes")
1017+
val ignore = new MessageActionItem("No")
1018+
def askToRegenerateConfigAndRestartBspMsg(file: String): String =
1019+
s"""|$file is outside of scala-cli build server scope.
1020+
|Would you like to fix this by regenerating bsp configuration and restarting the build sever?""".stripMargin
1021+
def askToRegenerateConfigAndRestartBsp(
1022+
file: Path
1023+
): ShowMessageRequestParams = {
1024+
val params = new ShowMessageRequestParams()
1025+
params.setMessage(
1026+
askToRegenerateConfigAndRestartBspMsg(
1027+
s"File: ${file.getFileName().toString()}"
1028+
)
1029+
)
1030+
params.setType(MessageType.Warning)
1031+
params.setActions(List(regenerateAndRestart, ignore).asJava)
1032+
params
1033+
}
1034+
}

metals/src/main/scala/scala/meta/internal/metals/MetalsLspService.scala

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import scala.meta.internal.bsp.BspConnector
2626
import scala.meta.internal.bsp.BspServers
2727
import scala.meta.internal.bsp.BspSession
2828
import scala.meta.internal.bsp.BuildChange
29+
import scala.meta.internal.bsp.ScalaCliBspScope
2930
import scala.meta.internal.builds.BloopInstall
3031
import scala.meta.internal.builds.BuildServerProvider
3132
import scala.meta.internal.builds.BuildTool
@@ -309,6 +310,7 @@ class MetalsLspService(
309310
folder,
310311
languageClient,
311312
shellRunner,
313+
statusBar,
312314
)
313315

314316
private val diagnostics: Diagnostics = new Diagnostics(
@@ -1043,7 +1045,10 @@ class MetalsLspService(
10431045
)
10441046
.ignoreValue
10451047
}
1046-
maybeImportScript(path).getOrElse(load())
1048+
for {
1049+
_ <- maybeAmendScalaCliBspConfig(path)
1050+
_ <- maybeImportScript(path).getOrElse(load())
1051+
} yield ()
10471052
}.asJava
10481053
}
10491054
}
@@ -1150,6 +1155,40 @@ class MetalsLspService(
11501155
.asJava
11511156
}
11521157

1158+
private def maybeAmendScalaCliBspConfig(file: AbsolutePath): Future[Unit] = {
1159+
def isScalaCli = bspSession.exists(_.main.isScalaCLI)
1160+
def isScalaFile =
1161+
file.toString.isScala || file.isJava || file.isAmmoniteScript
1162+
if (
1163+
isScalaCli && isScalaFile &&
1164+
buildTargets.inverseSources(file).isEmpty &&
1165+
file.toNIO.startsWith(folder.toNIO) &&
1166+
!ScalaCliBspScope.inScope(folder, file)
1167+
) {
1168+
languageClient
1169+
.showMessageRequest(
1170+
FileOutOfScalaCliBspScope.askToRegenerateConfigAndRestartBsp(
1171+
file.toNIO
1172+
)
1173+
)
1174+
.asScala
1175+
.flatMap {
1176+
case FileOutOfScalaCliBspScope.regenerateAndRestart =>
1177+
val buildTool =
1178+
ScalaCliBuildTool(folder, userConfig)
1179+
for {
1180+
_ <- buildTool.generateBspConfig(
1181+
folder,
1182+
bspConfigGenerator.runUnconditionally(buildTool, _),
1183+
statusBar,
1184+
)
1185+
_ <- quickConnectToBuildServer()
1186+
} yield ()
1187+
case _ => Future.successful(())
1188+
}
1189+
} else Future.successful(())
1190+
}
1191+
11531192
private def didCompileTarget(report: CompileReport): Unit = {
11541193
if (!isReliableFileWatcher) {
11551194
// NOTE(olafur) this step is exclusively used when running tests on
@@ -1879,6 +1918,7 @@ class MetalsLspService(
18791918
buildTool,
18801919
args,
18811920
),
1921+
statusBar,
18821922
)
18831923
.map(status => ensureAndConnect(buildTool, status))
18841924
case buildTools =>
@@ -1949,7 +1989,9 @@ class MetalsLspService(
19491989
for {
19501990
_ <- buildTool.createBspConfigIfNone(
19511991
folder,
1952-
args => bspConfigGenerator.runUnconditionally(buildTool, args),
1992+
args =>
1993+
bspConfigGenerator.runUnconditionally(buildTool, args),
1994+
statusBar,
19531995
)
19541996
_ = tables.buildServers.chooseServer(ScalaCliBuildTool.name)
19551997
buildChange <- quickConnectToBuildServer()

metals/src/main/scala/scala/meta/internal/metals/scalacli/ScalaCli.scala

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -421,14 +421,17 @@ object ScalaCli {
421421

422422
val scalaCliBspVersion = "2.1.0-M4"
423423

424-
def scalaCliBspJsonContent(args: List[String] = Nil): String = {
424+
def scalaCliBspJsonContent(
425+
args: List[String] = Nil,
426+
root: String = ".",
427+
): String = {
425428
val argv = List(
426429
ScalaCli.javaCommand,
427430
"-cp",
428431
ScalaCli.scalaCliClassPath().mkString(File.pathSeparator),
429432
ScalaCli.scalaCliMainClass,
430433
"bsp",
431-
".",
434+
root,
432435
) ++ args
433436
val bsjJson = ujson.Obj(
434437
"name" -> "scala-cli",

tests/slow/src/test/scala/tests/scalacli/ScalaCliSuite.scala

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,24 @@ package tests.scalacli
22

33
import scala.concurrent.Future
44

5+
import scala.meta.internal.metals.FileOutOfScalaCliBspScope
56
import scala.meta.internal.metals.Messages
7+
import scala.meta.internal.metals.MetalsEnrichments._
8+
import scala.meta.internal.metals.MetalsServerConfig
69
import scala.meta.internal.metals.ServerCommands
10+
import scala.meta.internal.metals.SlowTaskConfig
11+
import scala.meta.internal.metals.StatusBarConfig
12+
import scala.meta.internal.metals.scalacli.ScalaCli
713
import scala.meta.internal.metals.{BuildInfo => V}
814

915
import tests.FileLayout
1016

1117
class ScalaCliSuite extends BaseScalaCliSuite(V.scala3) {
18+
override def serverConfig: MetalsServerConfig =
19+
MetalsServerConfig.default.copy(
20+
slowTask = SlowTaskConfig.on,
21+
statusBar = StatusBarConfig.showMessage,
22+
)
1223

1324
private def simpleFileTest(useBsp: Boolean): Future[Unit] =
1425
for {
@@ -362,4 +373,65 @@ class ScalaCliSuite extends BaseScalaCliSuite(V.scala3) {
362373
} yield ()
363374
}
364375

376+
test("single-file-config") {
377+
cleanWorkspace()
378+
val msg = FileOutOfScalaCliBspScope.askToRegenerateConfigAndRestartBspMsg(
379+
"File: SomeFile.scala"
380+
)
381+
def workspaceMsgs =
382+
(server.client.messageRequests.asScala ++ server.client.showMessages.asScala
383+
.map(_.getMessage()))
384+
.collect {
385+
case `msg` => msg
386+
case msg @ "scala-cli bspConfig" => msg
387+
}
388+
.mkString("\n")
389+
def hasBuildTarget(fileName: String) = server.server.buildTargets
390+
.inverseSources(workspace.resolve(fileName))
391+
.isDefined
392+
for {
393+
_ <- scalaCliInitialize(useBsp = false)(
394+
s"""/src/Main.scala
395+
|object Main:
396+
| def foo = 3
397+
| val m = foo
398+
|/SomeFile.scala
399+
|object Other:
400+
| def foo = 3
401+
| val m = foo
402+
|/.bsp/scala-cli.json
403+
|${ScalaCli.scalaCliBspJsonContent(root = workspace.resolve("src/Main.scala").toString())}
404+
|/.scala-build/ide-inputs.json
405+
|${BaseScalaCliSuite.scalaCliIdeInputJson(".")}
406+
|""".stripMargin
407+
)
408+
_ <- server.didOpen("src/Main.scala")
409+
_ = assertNoDiff(workspaceMsgs, "")
410+
_ = assert(hasBuildTarget("src/Main.scala"))
411+
_ = assert(!hasBuildTarget("SomeFile.scala"))
412+
413+
_ <- server.didOpen("SomeFile.scala")
414+
_ <- server.server.buildServerPromise.future
415+
_ = assertNoDiff(workspaceMsgs, msg)
416+
_ = assert(!hasBuildTarget("SomeFile.scala"))
417+
418+
_ = server.client.regenerateAndRestartScalaCliBuildSever =
419+
FileOutOfScalaCliBspScope.regenerateAndRestart
420+
_ <- server.didOpen("SomeFile.scala")
421+
_ <- server.server.buildServerPromise.future
422+
_ = assertNoDiff(
423+
workspaceMsgs,
424+
List(msg, msg, "scala-cli bspConfig").mkString("\n"),
425+
)
426+
_ = assert(hasBuildTarget("src/Main.scala"))
427+
_ = assert(hasBuildTarget("SomeFile.scala"))
428+
429+
_ <- server.didOpen("SomeFile.scala")
430+
_ = assertNoDiff(
431+
workspaceMsgs,
432+
List(msg, msg, "scala-cli bspConfig").mkString("\n"),
433+
)
434+
} yield ()
435+
}
436+
365437
}

0 commit comments

Comments
 (0)