Skip to content

Commit

Permalink
improvement: add debug adapter for running main class to metals [skip…
Browse files Browse the repository at this point in the history
… ci]
  • Loading branch information
kasiaMarek committed May 7, 2024
1 parent a06a024 commit f09ed2d
Show file tree
Hide file tree
Showing 10 changed files with 501 additions and 20 deletions.
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ lazy val metals = project
V.lsp4j,
// for DAP
V.dap4j,
"ch.epfl.scala" %% "scala-debug-adapter" % V.debugAdapter,
// for finding paths of global log/cache directories
"dev.dirs" % "directories" % "26",
// for Java formatting
Expand Down Expand Up @@ -728,7 +729,6 @@ lazy val metalsDependencies = project
"ch.epfl.scala" % "bloop-maven-plugin" % V.mavenBloop,
"ch.epfl.scala" %% "gradle-bloop" % V.gradleBloop,
"com.sourcegraph" % "semanticdb-java" % V.javaSemanticdb,
"ch.epfl.scala" %% "scala-debug-adapter" % V.debugAdapter intransitive (),
"org.foundweekends.giter8" %% "giter8" % V.gitter8Version intransitive (),
),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1288,6 +1288,15 @@ object MetalsEnrichments
)
}

implicit class XtensionDebugSessionParams(params: b.DebugSessionParams) {
def asScalaMainClass(): Option[b.ScalaMainClass] =
params.getDataKind() match {
case b.DebugSessionParamsDataKind.SCALA_MAIN_CLASS =>
decodeJson(params.getData(), classOf[b.ScalaMainClass])
case _ => None
}
}

/**
* Strips ANSI colors.
* As long as the color codes are valid this should correctly strip
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,6 @@ final class RunTestCodeLens(
occurence: SymbolOccurrence,
textDocument: TextDocument,
target: BuildTargetIdentifier,
buildServerCanDebug: Boolean,
): Seq[l.Command] = {
if (occurence.symbol.endsWith("#main().")) {
textDocument.symbols
Expand All @@ -181,7 +180,6 @@ final class RunTestCodeLens(
Nil.asJava,
Nil.asJava,
),
buildServerCanDebug,
isJVM = true,
)
else
Expand Down Expand Up @@ -209,9 +207,9 @@ final class RunTestCodeLens(
commands = {
val main = classes.mainClasses
.get(symbol)
.map(mainCommand(target, _, buildServerCanDebug, isJVM))
.map(mainCommand(target, _, isJVM))
.getOrElse(Nil)
val tests =
lazy val tests =
// Currently tests can only be run via DAP
if (clientConfig.isDebuggingProvider() && buildServerCanDebug)
testClasses(target, classes, symbol, isJVM)
Expand All @@ -221,12 +219,12 @@ final class RunTestCodeLens(
.flatMap { symbol =>
classes.mainClasses
.get(symbol)
.map(mainCommand(target, _, buildServerCanDebug, isJVM))
.map(mainCommand(target, _, isJVM))
}
.getOrElse(Nil)
val javaMains =
if (path.isJava)
javaLenses(occurrence, textDocument, target, buildServerCanDebug)
javaLenses(occurrence, textDocument, target)
else Nil
main ++ tests ++ fromAnnot ++ javaMains
}
Expand Down Expand Up @@ -259,15 +257,15 @@ final class RunTestCodeLens(
val main =
classes.mainClasses
.get(expectedMainClass)
.map(mainCommand(target, _, buildServerCanDebug, isJVM))
.map(mainCommand(target, _, isJVM))
.getOrElse(Nil)

val fromAnnotations = textDocument.occurrences.flatMap { occ =>
for {
sym <- DebugProvider.mainFromAnnotation(occ, textDocument)
cls <- classes.mainClasses.get(sym)
range <- occurrenceRange(occ, distance)
} yield mainCommand(target, cls, buildServerCanDebug, isJVM).map { cmd =>
} yield mainCommand(target, cls, isJVM).map { cmd =>
new l.CodeLens(range, cmd, null)
}
}.flatten
Expand Down Expand Up @@ -324,7 +322,6 @@ final class RunTestCodeLens(
private def mainCommand(
target: b.BuildTargetIdentifier,
main: b.ScalaMainClass,
buildServerCanDebug: Boolean,
isJVM: Boolean,
): List[l.Command] = {
val javaBinary = buildTargets
Expand Down Expand Up @@ -352,7 +349,7 @@ final class RunTestCodeLens(
sessionParams(target, dataKind, data)
}

if (clientConfig.isDebuggingProvider() && buildServerCanDebug && isJVM)
if (clientConfig.isDebuggingProvider() && isJVM)
List(
command("run", StartRunSession, params),
command("debug", StartDebugSession, params),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,13 @@ import com.google.common.net.InetAddresses
import com.google.gson.JsonElement
import org.eclipse.lsp4j.MessageParams
import org.eclipse.lsp4j.MessageType
import scala.meta.internal.metals.debug.server.MetalsDebugToolsResolver
import ch.epfl.scala.debugadapter
import scala.concurrent.duration.Duration
import scala.meta.internal.metals.debug.server.MainClassDebugAdapter
import scala.meta.internal.metals.JdkSources
import scala.meta.internal.metals.debug.server.DebugeeParamsCreator
import scala.meta.internal.metals.MetalsEnrichments._

/**
* @param supportsTestSelection test selection hasn't been defined in BSP spec yet.
Expand All @@ -90,11 +97,14 @@ class DebugProvider(
sourceMapper: SourceMapper,
userConfig: () => UserConfiguration,
testProvider: TestSuitesProvider,
) extends Cancelable
)(implicit ec: ExecutionContext)
extends Cancelable
with LogForwarder {

import DebugProvider._

private val debugConfigCreator = new DebugeeParamsCreator(buildTargets)

private val runningLocal = new ju.concurrent.atomic.AtomicBoolean(false)

private val debugSessions = new MutableCancelable()
Expand Down Expand Up @@ -250,13 +260,13 @@ class DebugProvider(
val targets = parameters.getTargets().asScala.toSeq

compilations.compilationFinished(targets).flatMap { _ =>
val conn = buildServer
.startDebugSession(parameters, cancelPromise)
.map { uri =>
val socket = connect(uri)
connectedToServer.trySuccess(())
socket
}
val conn =
startDebugSession(buildServer, parameters, cancelPromise)
.map { uri =>
val socket = connect(uri)
connectedToServer.trySuccess(())
socket
}

conn
.withTimeout(60, TimeUnit.SECONDS)
Expand Down Expand Up @@ -311,6 +321,46 @@ class DebugProvider(
connectedToServer.future.map(_ => server)
}

private def startDebugSession(
buildServer: BuildServerConnection,
params: DebugSessionParams,
cancelPromise: Promise[Unit],
) =
if (buildServer.isDebuggingProvider) {
buildServer.startDebugSession(params, cancelPromise)
} else {
val debuggee = params.getDataKind() match {
case b.DebugSessionParamsDataKind.SCALA_MAIN_CLASS =>
val optDebuggee = for {
id <- params.getTargets().asScala.headOption
projectInfo <- debugConfigCreator.create(id)
scalaMainClass <- params.asScalaMainClass()
} yield {
new MainClassDebugAdapter(
workspace,
scalaMainClass,
projectInfo,
userConfig().javaHome,
)
}
optDebuggee.getOrElse(
throw new RuntimeException(s"Can't resolve debugee")
)
case _ => throw new RuntimeException(s"Can't resolve debugee")
}
val dapLogger = new scala.meta.internal.metals.debug.server.DebugLogger()
val resolver = new MetalsDebugToolsResolver()
val handler =
debugadapter.DebugServer.start(
debuggee,
resolver,
dapLogger,
autoCloseSession = true,
gracePeriod = Duration(5, TimeUnit.SECONDS),
)
Future.successful(handler.uri)
}

/**
* Given a BuildTargetIdentifier either get the displayName of that build
* target or default to the full URI to display to the user.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package scala.meta.internal.metals.debug.server

import ch.epfl.scala.debugadapter.Logger

class DebugLogger extends Logger {

override def debug(msg: => String): Unit = scribe.debug(msg)

override def info(msg: => String): Unit = scribe.info(msg)

override def warn(msg: => String): Unit = scribe.warn(msg)

override def error(msg: => String): Unit = scribe.error(msg)

override def trace(t: => Throwable): Unit =
scribe.trace(s"$t: ${t.getStackTrace().mkString("\n\t")}")

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package scala.meta.internal.metals.debug.server

import scala.meta.internal.metals.BuildTargets
import ch.epfl.scala.bsp4j.BuildTargetIdentifier
import ch.epfl.scala.debugadapter.Library
import ch.epfl.scala.debugadapter.UnmanagedEntry
import ch.epfl.scala.debugadapter.Module
import scala.meta.internal.metals.ScalaTarget
import scala.meta.internal.metals.MetalsEnrichments._
import ch.epfl.scala.debugadapter.ScalaVersion
import ch.epfl.scala.debugadapter.SourceDirectory
import ch.epfl.scala.bsp4j.MavenDependencyModule
import ch.epfl.scala.debugadapter.SourceJar
import scala.meta.io.AbsolutePath
import scala.meta.internal.metals.JavaTarget

class DebugeeParamsCreator(buildTargets: BuildTargets) {
def create(id: BuildTargetIdentifier) = {
val optScalaTarget = buildTargets.scalaTarget(id)
val optJavaTarget = buildTargets.javaTarget(id)
for {
name <- optScalaTarget
.map(_.displayName)
.orElse(optJavaTarget.map(_.displayName))
data <- buildTargets.targetData(id)
} yield {

val libraries = data.buildTargetDependencyModules
.get(id)
.filter(_.nonEmpty)
.getOrElse(Nil)
val debugLibs = libraries.flatMap(createLibrary(_))
val includedInLibs = debugLibs
.flatMap(_.sourceEntries.flatMap {
case SourceJar(jar) => Some(jar)
case _ => None
})
.toSet

val classpath = optScalaTarget
.map(_.scalac.classpath.toAbsoluteClasspath.toSeq)
.orElse {
buildTargets.targetClasspath(id).map(_.map(AbsolutePath(_)))
}
.getOrElse(Nil)

val filteredClassPath = classpath.collect {
case path if includedInLibs(path.toNIO) => UnmanagedEntry(path.toNIO)
}.toList

val modules = buildTargets
.allInverseDependencies(id)
.flatMap(id =>
buildTargets.scalaTarget(id).map(createModule(_)).orElse {
buildTargets.javaTarget(id).map(createModule(_))
}
)
.toSeq

new DebugeeProject(
buildTargets.scalaTarget(id).map(_.scalaVersion),
name,
modules,
libraries.flatMap(createLibrary(_)),
filteredClassPath,
classpath,
)
}
}

def createLibrary(lib: MavenDependencyModule) = {
def getWithClassifier(s: String) =
Option(lib.getArtifacts())
.flatMap(_.asScala.find(_.getClassifier() == s))
.flatMap(_.getUri().toAbsolutePathSafe)
for {
sources <- getWithClassifier("sources")
jar <- getWithClassifier(null)
} yield new Library(
lib.getName(),
lib.getVersion(),
jar.toNIO,
Seq(SourceJar(sources.toNIO)),
)
}

def createModule(target: ScalaTarget) = {
val scalaVersion = ScalaVersion(target.scalaVersion)
new Module(
target.displayName,
Some(scalaVersion),
target.scalac.getOptions().asScala.toSeq,
target.classDirectory.toAbsolutePath.toNIO,
sources(target.id),
)
}

def createModule(target: JavaTarget) =
new Module(
target.displayName,
None,
Nil,
target.classDirectory.toAbsolutePath.toNIO,
sources(target.id),
)

private def sources(id: BuildTargetIdentifier) =
buildTargets.sourceItemsToBuildTargets
.filter(_._2.iterator.asScala.contains(id))
.collect { case (path, _) =>
SourceDirectory(path.toNIO)
}
.toSeq
}

case class DebugeeProject(
scalaVersion: Option[String],
name: String,
modules: Seq[Module],
libraries: Seq[Library],
unmanagedEntries: Seq[UnmanagedEntry],
classpath: Seq[AbsolutePath],
)
Loading

0 comments on commit f09ed2d

Please sign in to comment.