Skip to content

Commit f116e13

Browse files
authored
improvement: add debug adapter for running main class to metals (#6383)
* improvement: add debug adapter for running main class to metals * add tests * wee cleanup * clean up * fix: use javacOptions classpath if scalacOptions are empty connected to: com-lihaoyi/mill#3086 * add more informative errors
1 parent 13f7ed7 commit f116e13

20 files changed

+612
-103
lines changed

build.sbt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,7 @@ lazy val metals = project
426426
V.lsp4j,
427427
// for DAP
428428
V.dap4j,
429+
"ch.epfl.scala" %% "scala-debug-adapter" % V.debugAdapter,
429430
// for finding paths of global log/cache directories
430431
"dev.dirs" % "directories" % "26",
431432
// for Java formatting
@@ -778,7 +779,6 @@ lazy val metalsDependencies = project
778779
"ch.epfl.scala" % "bloop-maven-plugin" % V.mavenBloop,
779780
"ch.epfl.scala" %% "gradle-bloop" % V.gradleBloop,
780781
"com.sourcegraph" % "semanticdb-java" % V.javaSemanticdb,
781-
"ch.epfl.scala" %% "scala-debug-adapter" % V.debugAdapter intransitive (),
782782
"org.foundweekends.giter8" %% "giter8" % V.gitter8Version intransitive (),
783783
),
784784
)

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,9 @@ final class BuildTargets private (
117117
def javaTarget(id: BuildTargetIdentifier): Option[JavaTarget] =
118118
data.fromOptions(_.javaTarget(id))
119119

120+
def jvmTarget(id: BuildTargetIdentifier): Option[JvmTarget] =
121+
data.fromOptions(_.jvmTarget(id))
122+
120123
def fullClasspath(
121124
id: BuildTargetIdentifier,
122125
cancelPromise: Promise[Unit],

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,14 @@ package scala.meta.internal.metals
33
import scala.meta.internal.metals.MetalsEnrichments._
44
import scala.meta.io.AbsolutePath
55

6+
import ch.epfl.scala.bsp4j.BuildTargetIdentifier
7+
68
trait JvmTarget {
79

10+
def displayName: String
11+
12+
def id: BuildTargetIdentifier
13+
814
/**
915
* If the build server supports lazy classpath resolution, we will
1016
* not get any classpath data eagerly and we should not
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package scala.meta.internal.metals
2+
3+
import java.nio.file.Files
4+
import java.nio.file.Path
5+
import java.util.jar.Attributes
6+
import java.util.jar.JarOutputStream
7+
import java.util.jar.Manifest
8+
9+
import scala.concurrent.ExecutionContext
10+
import scala.util.Using
11+
12+
import scala.meta.internal.metals.MetalsEnrichments._
13+
import scala.meta.internal.mtags.URIEncoderDecoder
14+
import scala.meta.internal.process.SystemProcess
15+
import scala.meta.io.AbsolutePath
16+
17+
object ManifestJar {
18+
def withTempManifestJar(
19+
classpath: Seq[Path]
20+
)(
21+
op: AbsolutePath => SystemProcess
22+
)(implicit ec: ExecutionContext): SystemProcess = {
23+
val manifestJar =
24+
createManifestJar(
25+
AbsolutePath(
26+
Files.createTempFile("jvm-forker-manifest", ".jar").toAbsolutePath
27+
),
28+
classpath,
29+
)
30+
31+
val process = op(manifestJar)
32+
process.complete.onComplete { case _ =>
33+
manifestJar.delete()
34+
}
35+
process
36+
}
37+
38+
def createManifestJar(
39+
manifestJar: AbsolutePath,
40+
classpath: Seq[Path],
41+
): AbsolutePath = {
42+
if (!manifestJar.exists) {
43+
manifestJar.touch()
44+
manifestJar.toNIO.toFile().deleteOnExit()
45+
}
46+
47+
val classpathStr =
48+
classpath
49+
.map(path => URIEncoderDecoder.encode(path.toUri().toString()))
50+
.mkString(" ")
51+
52+
val manifest = new Manifest()
53+
manifest.getMainAttributes.put(Attributes.Name.MANIFEST_VERSION, "1.0")
54+
manifest.getMainAttributes.put(Attributes.Name.CLASS_PATH, classpathStr)
55+
56+
val out = Files.newOutputStream(manifestJar.toNIO)
57+
Using.resource(new JarOutputStream(out, manifest))(identity)
58+
manifestJar
59+
}
60+
61+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1310,6 +1310,19 @@ object MetalsEnrichments
13101310
)
13111311
}
13121312

1313+
implicit class XtensionDebugSessionParams(params: b.DebugSessionParams) {
1314+
def asScalaMainClass(): Either[String, b.ScalaMainClass] =
1315+
params.getDataKind() match {
1316+
case b.DebugSessionParamsDataKind.SCALA_MAIN_CLASS =>
1317+
decodeJson(params.getData(), classOf[b.ScalaMainClass])
1318+
.toRight(s"Cannot decode $params as `ScalaMainClass`.")
1319+
case _ =>
1320+
Left(
1321+
s"Cannot decode params as `ScalaMainClass` incorrect data kind: ${params.getDataKind()}."
1322+
)
1323+
}
1324+
}
1325+
13131326
/**
13141327
* Strips ANSI colors.
13151328
* As long as the color codes are valid this should correctly strip

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ final class TargetData {
109109
javaTargetInfo.get(id)
110110
def jvmTarget(id: BuildTargetIdentifier): Option[JvmTarget] =
111111
scalaTarget(id).orElse(javaTarget(id))
112+
def jvmTargets(id: BuildTargetIdentifier): List[JvmTarget] =
113+
List(scalaTarget(id), javaTarget(id)).flatten
112114

113115
private val sourceBuildTargetsCache =
114116
new util.concurrent.ConcurrentHashMap[AbsolutePath, Option[
@@ -163,7 +165,8 @@ final class TargetData {
163165
}
164166
} yield path
165167

166-
if (fromDepModules.isEmpty) jvmTarget(id).flatMap(_.jarClasspath)
168+
if (fromDepModules.isEmpty)
169+
jvmTargets(id).flatMap(_.jarClasspath).headOption
167170
else Some(fromDepModules)
168171
}
169172

metals/src/main/scala/scala/meta/internal/metals/codelenses/RunTestCodeLens.scala

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ final class RunTestCodeLens(
165165
occurence: SymbolOccurrence,
166166
textDocument: TextDocument,
167167
target: BuildTargetIdentifier,
168-
buildServerCanDebug: Boolean,
169168
): Seq[l.Command] = {
170169
if (occurence.symbol.endsWith("#main().")) {
171170
textDocument.symbols
@@ -182,7 +181,6 @@ final class RunTestCodeLens(
182181
Nil.asJava,
183182
Nil.asJava,
184183
),
185-
buildServerCanDebug,
186184
isJVM = true,
187185
)
188186
else
@@ -210,9 +208,9 @@ final class RunTestCodeLens(
210208
commands = {
211209
val main = classes.mainClasses
212210
.get(symbol)
213-
.map(mainCommand(target, _, buildServerCanDebug, isJVM))
211+
.map(mainCommand(target, _, isJVM))
214212
.getOrElse(Nil)
215-
val tests =
213+
lazy val tests =
216214
// Currently tests can only be run via DAP
217215
if (clientConfig.isDebuggingProvider() && buildServerCanDebug)
218216
testClasses(target, classes, symbol, isJVM)
@@ -222,12 +220,12 @@ final class RunTestCodeLens(
222220
.flatMap { symbol =>
223221
classes.mainClasses
224222
.get(symbol)
225-
.map(mainCommand(target, _, buildServerCanDebug, isJVM))
223+
.map(mainCommand(target, _, isJVM))
226224
}
227225
.getOrElse(Nil)
228226
val javaMains =
229227
if (path.isJava)
230-
javaLenses(occurrence, textDocument, target, buildServerCanDebug)
228+
javaLenses(occurrence, textDocument, target)
231229
else Nil
232230
main ++ tests ++ fromAnnot ++ javaMains
233231
}
@@ -260,15 +258,15 @@ final class RunTestCodeLens(
260258
val main =
261259
classes.mainClasses
262260
.get(expectedMainClass)
263-
.map(mainCommand(target, _, buildServerCanDebug, isJVM))
261+
.map(mainCommand(target, _, isJVM))
264262
.getOrElse(Nil)
265263

266264
val fromAnnotations = textDocument.occurrences.flatMap { occ =>
267265
for {
268266
sym <- DebugProvider.mainFromAnnotation(occ, textDocument)
269267
cls <- classes.mainClasses.get(sym)
270268
range <- occurrenceRange(occ, distance)
271-
} yield mainCommand(target, cls, buildServerCanDebug, isJVM).map { cmd =>
269+
} yield mainCommand(target, cls, isJVM).map { cmd =>
272270
new l.CodeLens(range, cmd, null)
273271
}
274272
}.flatten
@@ -325,7 +323,6 @@ final class RunTestCodeLens(
325323
private def mainCommand(
326324
target: b.BuildTargetIdentifier,
327325
main: b.ScalaMainClass,
328-
buildServerCanDebug: Boolean,
329326
isJVM: Boolean,
330327
): List[l.Command] = {
331328
val javaBinary = buildTargets
@@ -353,7 +350,7 @@ final class RunTestCodeLens(
353350
sessionParams(target, dataKind, data)
354351
}
355352

356-
if (clientConfig.isDebuggingProvider() && buildServerCanDebug && isJVM)
353+
if (clientConfig.isDebuggingProvider() && isJVM)
357354
List(
358355
command("run", StartRunSession, params),
359356
command("debug", StartDebugSession, params),

metals/src/main/scala/scala/meta/internal/metals/debug/DebugProvider.scala

Lines changed: 67 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import scala.collection.concurrent.TrieMap
1313
import scala.concurrent.ExecutionContext
1414
import scala.concurrent.Future
1515
import scala.concurrent.Promise
16+
import scala.concurrent.duration.Duration
1617
import scala.util.Failure
1718
import scala.util.Success
1819
import scala.util.Try
@@ -51,6 +52,11 @@ import scala.meta.internal.metals.clients.language.MetalsQuickPickParams
5152
import scala.meta.internal.metals.clients.language.MetalsStatusParams
5253
import scala.meta.internal.metals.config.RunType
5354
import scala.meta.internal.metals.config.RunType._
55+
import scala.meta.internal.metals.debug.server.DebugLogger
56+
import scala.meta.internal.metals.debug.server.DebugeeParamsCreator
57+
import scala.meta.internal.metals.debug.server.MainClassDebugAdapter
58+
import scala.meta.internal.metals.debug.server.MetalsDebugToolsResolver
59+
import scala.meta.internal.metals.debug.server.MetalsDebuggee
5460
import scala.meta.internal.metals.testProvider.TestSuitesProvider
5561
import scala.meta.internal.mtags.DefinitionAlternatives.GlobalSymbol
5662
import scala.meta.internal.mtags.OnDemandSymbolIndex
@@ -65,6 +71,7 @@ import ch.epfl.scala.bsp4j.BuildTargetIdentifier
6571
import ch.epfl.scala.bsp4j.DebugSessionParams
6672
import ch.epfl.scala.bsp4j.ScalaMainClass
6773
import ch.epfl.scala.{bsp4j => b}
74+
import ch.epfl.scala.{debugadapter => dap}
6875
import com.google.common.net.InetAddresses
6976
import com.google.gson.JsonElement
7077
import org.eclipse.lsp4j.MessageParams
@@ -91,11 +98,14 @@ class DebugProvider(
9198
sourceMapper: SourceMapper,
9299
userConfig: () => UserConfiguration,
93100
testProvider: TestSuitesProvider,
94-
) extends Cancelable
101+
)(implicit ec: ExecutionContext)
102+
extends Cancelable
95103
with LogForwarder {
96104

97105
import DebugProvider._
98106

107+
private val debugConfigCreator = new DebugeeParamsCreator(buildTargets)
108+
99109
private val runningLocal = new ju.concurrent.atomic.AtomicBoolean(false)
100110

101111
private val debugSessions = new MutableCancelable()
@@ -251,13 +261,13 @@ class DebugProvider(
251261
val targets = parameters.getTargets().asScala.toSeq
252262

253263
compilations.compilationFinished(targets).flatMap { _ =>
254-
val conn = buildServer
255-
.startDebugSession(parameters, cancelPromise)
256-
.map { uri =>
257-
val socket = connect(uri)
258-
connectedToServer.trySuccess(())
259-
socket
260-
}
264+
val conn =
265+
startDebugSession(buildServer, parameters, cancelPromise)
266+
.map { uri =>
267+
val socket = connect(uri)
268+
connectedToServer.trySuccess(())
269+
socket
270+
}
261271

262272
val startupTimeout = clientConfig.initialConfig.debugServerStartTimeout
263273

@@ -314,6 +324,55 @@ class DebugProvider(
314324
connectedToServer.future.map(_ => server)
315325
}
316326

327+
private def startDebugSession(
328+
buildServer: BuildServerConnection,
329+
params: DebugSessionParams,
330+
cancelPromise: Promise[Unit],
331+
) =
332+
if (buildServer.isDebuggingProvider || buildServer.isSbt) {
333+
buildServer.startDebugSession(params, cancelPromise)
334+
} else {
335+
def getDebugee: Either[String, MetalsDebuggee] =
336+
params.getDataKind() match {
337+
case b.DebugSessionParamsDataKind.SCALA_MAIN_CLASS =>
338+
for {
339+
id <- params
340+
.getTargets()
341+
.asScala
342+
.headOption
343+
.toRight(s"Missing build target in debug params.")
344+
projectInfo <- debugConfigCreator.create(id)
345+
scalaMainClass <- params.asScalaMainClass()
346+
} yield new MainClassDebugAdapter(
347+
workspace,
348+
scalaMainClass,
349+
projectInfo,
350+
userConfig().javaHome,
351+
)
352+
case kind =>
353+
Left(s"Starting debug session for $kind in not supported.")
354+
}
355+
356+
for {
357+
_ <- compilations.compileTargets(params.getTargets().asScala.toSeq)
358+
} yield {
359+
val debuggee = getDebugee match {
360+
case Right(debuggee) => debuggee
361+
case Left(errorMessage) => throw new RuntimeException(errorMessage)
362+
}
363+
val dapLogger = new DebugLogger()
364+
val resolver = new MetalsDebugToolsResolver()
365+
val handler =
366+
dap.DebugServer.run(
367+
debuggee,
368+
resolver,
369+
dapLogger,
370+
gracePeriod = Duration(5, TimeUnit.SECONDS),
371+
)
372+
handler.uri
373+
}
374+
}
375+
317376
/**
318377
* Given a BuildTargetIdentifier either get the displayName of that build
319378
* target or default to the full URI to display to the user.

0 commit comments

Comments
 (0)