diff --git a/bsp/src/mill/bsp/BspContext.scala b/bsp/src/mill/bsp/BspContext.scala index 9b5cadcbbd2..b93ce2d187c 100644 --- a/bsp/src/mill/bsp/BspContext.scala +++ b/bsp/src/mill/bsp/BspContext.scala @@ -65,6 +65,7 @@ private[mill] class BspContext( BspWorker(mill.api.WorkspaceRoot.workspaceRoot, home, log).flatMap { worker => os.makeDir.all(home / Constants.bspDir) worker.startBspServer( + mill.api.WorkspaceRoot.workspaceRoot, streams, logStream.getOrElse(streams.err), home / Constants.bspDir, diff --git a/bsp/src/mill/bsp/BspWorker.scala b/bsp/src/mill/bsp/BspWorker.scala index c8c435c6f18..f24a3b6c3a9 100644 --- a/bsp/src/mill/bsp/BspWorker.scala +++ b/bsp/src/mill/bsp/BspWorker.scala @@ -8,6 +8,7 @@ import java.net.URL private trait BspWorker { def startBspServer( + topLevelBuildRoot: os.Path, streams: SystemStreams, logStream: PrintStream, logDir: os.Path, diff --git a/bsp/worker/src/mill/bsp/worker/BspWorkerImpl.scala b/bsp/worker/src/mill/bsp/worker/BspWorkerImpl.scala index b48d244a5d1..9bbb172c77c 100644 --- a/bsp/worker/src/mill/bsp/worker/BspWorkerImpl.scala +++ b/bsp/worker/src/mill/bsp/worker/BspWorkerImpl.scala @@ -15,6 +15,7 @@ import scala.concurrent.{Await, CancellationException, Promise} private class BspWorkerImpl() extends BspWorker { override def startBspServer( + topLevelBuildRoot: os.Path, streams: SystemStreams, logStream: PrintStream, logDir: os.Path, @@ -23,6 +24,7 @@ private class BspWorkerImpl() extends BspWorker { val millServer = new MillBuildServer( + topLevelProjectRoot = topLevelBuildRoot, bspVersion = Constants.bspProtocolVersion, serverVersion = BuildInfo.millVersion, serverName = Constants.serverName, diff --git a/bsp/worker/src/mill/bsp/worker/MillBuildServer.scala b/bsp/worker/src/mill/bsp/worker/MillBuildServer.scala index acaa2a0ebcb..e10f60c8702 100644 --- a/bsp/worker/src/mill/bsp/worker/MillBuildServer.scala +++ b/bsp/worker/src/mill/bsp/worker/MillBuildServer.scala @@ -1,87 +1,31 @@ package mill.bsp.worker -import ch.epfl.scala.bsp4j.{ - BuildClient, - BuildServer, - BuildServerCapabilities, - BuildTarget, - BuildTargetCapabilities, - BuildTargetIdentifier, - CleanCacheParams, - CleanCacheResult, - CompileParams, - CompileProvider, - CompileResult, - DebugProvider, - DebugSessionAddress, - DebugSessionParams, - DependencyModule, - DependencyModulesItem, - DependencyModulesParams, - DependencyModulesResult, - DependencySourcesItem, - DependencySourcesParams, - DependencySourcesResult, - InitializeBuildParams, - InitializeBuildResult, - InverseSourcesParams, - InverseSourcesResult, - LogMessageParams, - MessageType, - OutputPathItem, - OutputPathItemKind, - OutputPathsItem, - OutputPathsParams, - OutputPathsResult, - ReadParams, - ResourcesItem, - ResourcesParams, - ResourcesResult, - RunParams, - RunProvider, - RunResult, - SourceItem, - SourceItemKind, - SourcesItem, - SourcesParams, - SourcesResult, - StatusCode, - TaskFinishDataKind, - TaskFinishParams, - TaskId, - TaskStartDataKind, - TaskStartParams, - TestParams, - TestProvider, - TestResult, - TestTask, - WorkspaceBuildTargetsResult -} import ch.epfl.scala.bsp4j +import ch.epfl.scala.bsp4j._ import com.google.gson.JsonObject import mill.T import mill.api.{DummyTestReporter, Result, Strict} +import mill.bsp.BspServerResult +import mill.bsp.worker.Utils.{makeBuildTarget, outputPaths, sanitizeUri} import mill.define.Segment.Label import mill.define.{Args, Discover, ExternalModule, Task} import mill.eval.Evaluator +import mill.eval.Evaluator.TaskResult import mill.main.MainModule -import mill.scalalib.{JavaModule, SemanticDbJavaModule, TestModule} -import mill.scalalib.bsp.{BspModule, JvmBuildTarget, ScalaBuildTarget} import mill.runner.MillBuildRootModule +import mill.scalalib.bsp.{BspModule, JvmBuildTarget, ScalaBuildTarget} +import mill.scalalib.{JavaModule, SemanticDbJavaModule, TestModule} import java.io.PrintStream import java.util.concurrent.CompletableFuture import scala.concurrent.Promise import scala.jdk.CollectionConverters._ import scala.reflect.ClassTag -import scala.util.{Failure, Success, Try} -import Utils.sanitizeUri -import mill.bsp.BspServerResult -import mill.eval.Evaluator.TaskResult - import scala.util.chaining.scalaUtilChainingOps import scala.util.control.NonFatal +import scala.util.{Failure, Success, Try} private class MillBuildServer( + topLevelProjectRoot: os.Path, bspVersion: String, serverVersion: String, serverName: String, @@ -110,7 +54,7 @@ private class MillBuildServer( if (statePromise.isCompleted) statePromise = Promise[State]() // replace the promise evaluatorsOpt.foreach { evaluators => statePromise.success( - new State(evaluators, debug) + new State(topLevelProjectRoot, evaluators, debug) ) } } @@ -209,7 +153,7 @@ private class MillBuildServer( } override def workspaceBuildTargets(): CompletableFuture[WorkspaceBuildTargetsResult] = - completableTasks( + completableTasksWithState( "workspaceBuildTargets", targetIds = _.bspModulesById.keySet.toSeq, tasks = { case m: BspModule => m.bspBuildTargetData } @@ -246,30 +190,13 @@ private class MillBuildServer( } val bt = m.bspBuildTarget - val buildTarget = new BuildTarget( - id, - bt.tags.asJava, - bt.languageIds.asJava, - depsIds.asJava, - new BuildTargetCapabilities().tap { it => - it.setCanCompile(bt.canCompile) - it.setCanTest(bt.canTest) - it.setCanRun(bt.canRun) - it.setCanDebug(bt.canDebug) - } - ) - - bt.displayName.foreach(buildTarget.setDisplayName) - bt.baseDirectory.foreach(p => buildTarget.setBaseDirectory(sanitizeUri(p))) - - for ((dataKind, data) <- data) { - buildTarget.setDataKind(dataKind) - buildTarget.setData(data) - } - - buildTarget + makeBuildTarget(id, depsIds, bt, data) - }(new WorkspaceBuildTargetsResult(_)) + } { (targets, state) => + new WorkspaceBuildTargetsResult( + (targets.asScala ++ state.syntheticRootBspBuildTarget.map(_.target)).asJava + ) + } override def workspaceReload(): CompletableFuture[Object] = completableNoState("workspaceReload", false) { @@ -298,7 +225,7 @@ private class MillBuildServer( generated ) - completableTasks( + completableTasksWithState( hint = s"buildTargetSources ${sourcesParams}", targetIds = _ => sourcesParams.getTargets.asScala.toSeq, tasks = { @@ -316,8 +243,10 @@ private class MillBuildServer( } ) { case (ev, state, id, module, items) => new SourcesItem(id, items.asJava) - } { - new SourcesResult(_) + } { (sourceItems, state) => + new SourcesResult( + (sourceItems.asScala.toSeq ++ state.syntheticRootBspBuildTarget.map(_.synthSources)).asJava + ) } } @@ -451,6 +380,7 @@ private class MillBuildServer( // already has some from the build file, what to do? override def buildTargetCompile(p: CompileParams): CompletableFuture[CompileResult] = completable(s"buildTargetCompile ${p}") { state => + p.setTargets(state.filterNonSynthetic(p.getTargets)) val params = TaskParameters.fromCompileParams(p) val taskId = params.hashCode() val compileTasksEvs = params.getTargets.distinct.map(state.bspModulesById).map { @@ -483,29 +413,29 @@ private class MillBuildServer( override def buildTargetOutputPaths(params: OutputPathsParams) : CompletableFuture[OutputPathsResult] = completable(s"buildTargetOutputPaths ${params}") { state => + val synthOutpaths = for { + synthTarget <- state.syntheticRootBspBuildTarget + if params.getTargets.contains(synthTarget.id) + baseDir <- synthTarget.bt.baseDirectory + } yield new OutputPathsItem(synthTarget.id, outputPaths(baseDir, topLevelProjectRoot).asJava) + val items = for { target <- params.getTargets.asScala (module, ev) <- state.bspModulesById.get(target) } yield { val items = - if (module.millOuterCtx.external) List( - new OutputPathItem( - // Spec says, a directory must end with a forward slash - sanitizeUri(ev.externalOutPath) + "/", - OutputPathItemKind.DIRECTORY - ) - ) - else List( - new OutputPathItem( - // Spec says, a directory must end with a forward slash - sanitizeUri(ev.outPath) + "/", - OutputPathItemKind.DIRECTORY + if (module.millOuterCtx.external) + outputPaths( + module.bspBuildTarget.baseDirectory.get, + topLevelProjectRoot ) - ) + else + outputPaths(module.bspBuildTarget.baseDirectory.get, topLevelProjectRoot) + new OutputPathsItem(target, items.asJava) } - new OutputPathsResult(items.asJava) + new OutputPathsResult((items ++ synthOutpaths).asJava) } override def buildTargetRun(runParams: RunParams): CompletableFuture[RunResult] = @@ -536,6 +466,7 @@ private class MillBuildServer( override def buildTargetTest(testParams: TestParams): CompletableFuture[TestResult] = completable(s"buildTargetTest ${testParams}") { state => + testParams.setTargets(state.filterNonSynthetic(testParams.getTargets)) val millBuildTargetIds = state .rootModules .map { case m: BspModule => state.bspIdByModule(m) } @@ -627,6 +558,7 @@ private class MillBuildServer( override def buildTargetCleanCache(cleanCacheParams: CleanCacheParams) : CompletableFuture[CleanCacheResult] = completable(s"buildTargetCleanCache ${cleanCacheParams}") { state => + cleanCacheParams.setTargets(state.filterNonSynthetic(cleanCacheParams.getTargets)) val (msg, cleaned) = cleanCacheParams.getTargets.asScala.foldLeft(( "", @@ -675,6 +607,7 @@ private class MillBuildServer( override def debugSessionStart(debugParams: DebugSessionParams) : CompletableFuture[DebugSessionAddress] = completable(s"debugSessionStart ${debugParams}") { state => + debugParams.setTargets(state.filterNonSynthetic(debugParams.getTargets)) throw new NotImplementedError("debugSessionStart endpoint is not implemented") } @@ -687,10 +620,24 @@ private class MillBuildServer( targetIds: State => Seq[BuildTargetIdentifier], tasks: PartialFunction[BspModule, Task[W]] )(f: (Evaluator, State, BuildTargetIdentifier, BspModule, W) => T)(agg: java.util.List[T] => V) - : CompletableFuture[V] = { + : CompletableFuture[V] = + completableTasksWithState[T, V, W](hint, targetIds, tasks)(f)((l, _) => agg(l)) + + /** + * @params tasks A partial function + * @param f The function must accept the same modules as the partial function given by `tasks`. + */ + def completableTasksWithState[T, V, W: ClassTag]( + hint: String, + targetIds: State => Seq[BuildTargetIdentifier], + tasks: PartialFunction[BspModule, Task[W]] + )(f: (Evaluator, State, BuildTargetIdentifier, BspModule, W) => T)(agg: ( + java.util.List[T], + State + ) => V): CompletableFuture[V] = { val prefix = hint.split(" ").head completable(hint) { state: State => - val ids = targetIds(state) + val ids = state.filterNonSynthetic(targetIds(state).asJava).asScala val tasksSeq = ids.flatMap { id => val (m, ev) = state.bspModulesById(id) tasks.lift.apply(m).map(ts => (ts, (ev, id))) @@ -729,7 +676,7 @@ private class MillBuildServer( } } - agg(evaluated.flatten.toSeq.asJava) + agg(evaluated.flatten.toSeq.asJava, state) } } diff --git a/bsp/worker/src/mill/bsp/worker/State.scala b/bsp/worker/src/mill/bsp/worker/State.scala index d5efe434481..c3b70f322f1 100644 --- a/bsp/worker/src/mill/bsp/worker/State.scala +++ b/bsp/worker/src/mill/bsp/worker/State.scala @@ -6,7 +6,7 @@ import mill.scalalib.internal.JavaModuleUtils import mill.define.Module import mill.eval.Evaluator -private class State(evaluators: Seq[Evaluator], debug: String => Unit) { +private class State(workspaceDir: os.Path, evaluators: Seq[Evaluator], debug: String => Unit) { lazy val bspModulesById: Map[BuildTargetIdentifier, (BspModule, Evaluator)] = { val modules: Seq[(Module, Seq[Module], Evaluator)] = evaluators .flatMap(ev => ev.rootModules.map(rm => (rm, JavaModuleUtils.transitiveModules(rm), ev))) @@ -34,4 +34,12 @@ private class State(evaluators: Seq[Evaluator], debug: String => Unit) { lazy val bspIdByModule: Map[BspModule, BuildTargetIdentifier] = bspModulesById.view.mapValues(_._1).map(_.swap).toMap + lazy val syntheticRootBspBuildTarget: Option[SyntheticRootBspBuildTargetData] = + SyntheticRootBspBuildTargetData.makeIfNeeded(bspModulesById.values.map(_._1), workspaceDir) + + def filterNonSynthetic(input: java.util.List[BuildTargetIdentifier]) + : java.util.List[BuildTargetIdentifier] = { + import collection.JavaConverters._ + input.asScala.filterNot(syntheticRootBspBuildTarget.map(_.id).contains).toList.asJava + } } diff --git a/bsp/worker/src/mill/bsp/worker/SyntheticRootBspBuildTargetData.scala b/bsp/worker/src/mill/bsp/worker/SyntheticRootBspBuildTargetData.scala new file mode 100644 index 00000000000..45302e0ce97 --- /dev/null +++ b/bsp/worker/src/mill/bsp/worker/SyntheticRootBspBuildTargetData.scala @@ -0,0 +1,48 @@ +package mill.bsp.worker + +import ch.epfl.scala.bsp4j.{BuildTargetIdentifier, SourceItem, SourceItemKind, SourcesItem} +import mill.bsp.worker.Utils.{makeBuildTarget, sanitizeUri} +import mill.scalalib.bsp.{BspBuildTarget, BspModule} +import mill.scalalib.bsp.BspModule.Tag + +import java.util.UUID +import scala.jdk.CollectionConverters._ +import ch.epfl.scala.bsp4j.BuildTarget + +/** + * Synthesised [[BspBuildTarget]] to handle exclusions. + * Intellij-Bsp doesn't provide a way to exclude files outside of module,so if there is no module having content root of [[topLevelProjectRoot]], [[SyntheticRootBspBuildTargetData]] will be created + */ +class SyntheticRootBspBuildTargetData(topLevelProjectRoot: os.Path) { + val id: BuildTargetIdentifier = new BuildTargetIdentifier( + Utils.sanitizeUri(topLevelProjectRoot / s"synth-build-target-${UUID.randomUUID()}") + ) + + val bt: BspBuildTarget = BspBuildTarget( + displayName = Some(topLevelProjectRoot.last + "-root"), + baseDirectory = Some(topLevelProjectRoot), + tags = Seq(Tag.Manual), + languageIds = Seq.empty, + canCompile = false, + canTest = false, + canRun = false, + canDebug = false + ) + + val target: BuildTarget = makeBuildTarget(id, Seq.empty, bt, None) + private val sourcePath = topLevelProjectRoot / "src" + def synthSources = new SourcesItem( + id, + Seq(new SourceItem(sanitizeUri(sourcePath), SourceItemKind.DIRECTORY, false)).asJava + ) // intellijBSP does not create contentRootData for module with only outputPaths (this is probably a bug) +} +object SyntheticRootBspBuildTargetData { + def makeIfNeeded( + existingModules: Iterable[BspModule], + workspaceDir: os.Path + ): Option[SyntheticRootBspBuildTargetData] = { + def containsWorkspaceDir(path: Option[os.Path]) = path.exists(workspaceDir.startsWith) + if (existingModules.exists { m => containsWorkspaceDir(m.bspBuildTarget.baseDirectory) }) None + else Some(new SyntheticRootBspBuildTargetData(workspaceDir)) + } +} diff --git a/bsp/worker/src/mill/bsp/worker/Utils.scala b/bsp/worker/src/mill/bsp/worker/Utils.scala index 42a06af854c..5fe3f57b190 100644 --- a/bsp/worker/src/mill/bsp/worker/Utils.scala +++ b/bsp/worker/src/mill/bsp/worker/Utils.scala @@ -1,11 +1,23 @@ package mill.bsp.worker -import ch.epfl.scala.bsp4j.{BuildClient, BuildTargetIdentifier, StatusCode, TaskId} +import ch.epfl.scala.bsp4j.{ + BuildClient, + BuildTarget, + BuildTargetCapabilities, + BuildTargetIdentifier, + OutputPathItem, + OutputPathItemKind, + StatusCode, + TaskId +} import mill.api.{CompileProblemReporter, PathRef} import mill.api.Result.{Skipped, Success} import mill.eval.Evaluator import mill.scalalib.JavaModule -import mill.scalalib.bsp.BspModule +import mill.scalalib.bsp.{BspBuildTarget, BspModule} + +import scala.jdk.CollectionConverters._ +import scala.util.chaining.scalaUtilChainingOps private object Utils { @@ -46,6 +58,54 @@ private object Utils { else StatusCode.OK } + def makeBuildTarget( + id: BuildTargetIdentifier, + depsIds: Seq[BuildTargetIdentifier], + bt: BspBuildTarget, + data: Option[(String, Object)] + ): BuildTarget = { + val buildTarget = new BuildTarget( + id, + bt.tags.asJava, + bt.languageIds.asJava, + depsIds.asJava, + new BuildTargetCapabilities().tap { it => + it.setCanCompile(bt.canCompile) + it.setCanTest(bt.canTest) + it.setCanRun(bt.canRun) + it.setCanDebug(bt.canDebug) + } + ) + + bt.displayName.foreach(buildTarget.setDisplayName) + bt.baseDirectory.foreach(p => buildTarget.setBaseDirectory(sanitizeUri(p))) + + for ((dataKind, data) <- data) { + buildTarget.setDataKind(dataKind) + buildTarget.setData(data) + } + buildTarget + } + + def outputPaths( + buildTargetBaseDir: os.Path, + topLevelProjectRoot: os.Path + ): Seq[OutputPathItem] = { + + def outputPathItem(path: os.Path) = + // Spec says, a directory must end with a forward slash + new OutputPathItem(sanitizeUri(path) + "/", OutputPathItemKind.DIRECTORY) + + if (topLevelProjectRoot.startsWith(buildTargetBaseDir)) + Seq( + outputPathItem(topLevelProjectRoot / ".idea"), + outputPathItem(topLevelProjectRoot / "out"), + outputPathItem(topLevelProjectRoot / ".bsp"), + outputPathItem(topLevelProjectRoot / ".bloop") + ) + else Nil + } + private[this] def getStatusCodePerTask( results: Evaluator.Results, task: mill.define.Task[_]