diff --git a/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala index addcbb041..78e7d849f 100644 --- a/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala +++ b/main-command/src/main/scala/sbt/internal/server/ServerHandler.scala @@ -12,9 +12,8 @@ package server import sjsonnew.JsonFormat import sbt.internal.protocol._ import sbt.util.Logger -import sbt.protocol.{ SettingQuery => Q, CompletionParams => CP } +import sbt.protocol.{ CompletionParams => CP, SettingQuery => Q } import sbt.internal.langserver.{ CancelRequestParams => CRP } -import sbt.internal.bsp._ /** * ServerHandler allows plugins to extend sbt server. @@ -65,6 +64,7 @@ trait ServerCallback { def jsonRpcRespondError(execId: Option[String], code: Long, message: String): Unit def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit def appendExec(exec: Exec): Boolean + def appendExec(commandLine: String, requestId: Option[String]): Boolean def log: Logger def name: String @@ -74,6 +74,4 @@ trait ServerCallback { private[sbt] def onSettingQuery(execId: Option[String], req: Q): Unit private[sbt] def onCompletionRequest(execId: Option[String], cp: CP): Unit private[sbt] def onCancellationRequest(execId: Option[String], crp: CRP): Unit - private[sbt] def onBspInitialize(execId: Option[String], param: InitializeBuildParams): Unit - private[sbt] def onBspBuildTargets(execId: Option[String]): Unit } diff --git a/main/src/main/scala/sbt/Defaults.scala b/main/src/main/scala/sbt/Defaults.scala index afb1bc3f4..006074272 100755 --- a/main/src/main/scala/sbt/Defaults.scala +++ b/main/src/main/scala/sbt/Defaults.scala @@ -199,9 +199,10 @@ object Defaults extends BuildCommon { }, fileConverter := MappedFileConverter(rootPaths.value, allowMachinePath.value), fullServerHandlers := { - (Vector(LanguageServerProtocol.handler(fileConverter.value)) - ++ serverHandlers.value - ++ Vector(ServerHandler.fallback)) + Seq( + LanguageServerProtocol.handler(fileConverter.value), + BuildServerProtocol.handler(sbtVersion.value) + ) ++ serverHandlers.value :+ ServerHandler.fallback }, uncachedStamper := Stamps.uncachedStamps(fileConverter.value), reusableStamper := Stamps.timeWrapLibraryStamps(uncachedStamper.value, fileConverter.value), diff --git a/main/src/main/scala/sbt/Keys.scala b/main/src/main/scala/sbt/Keys.scala index f8f340796..683c526a5 100644 --- a/main/src/main/scala/sbt/Keys.scala +++ b/main/src/main/scala/sbt/Keys.scala @@ -338,7 +338,10 @@ object Keys { val internalDependencyConfigurations = settingKey[Seq[(ProjectRef, Set[String])]]("The project configurations that this configuration depends on") val closeClassLoaders = settingKey[Boolean]("Close classloaders in run and test when the task completes.").withRank(DSetting) val allowZombieClassLoaders = settingKey[Boolean]("Allow a classloader that has previously been closed by `run` or `test` to continue loading classes.") + val buildTargetIdentifier = settingKey[BuildTargetIdentifier]("Id for BSP build target.").withRank(DSetting) + val bspWorkspace = taskKey[Map[BuildTargetIdentifier, Scope]]("Mapping of BSP build targets to sbt scopes").withRank(DTask) + val bspWorkspaceBuildTargets = taskKey[Seq[BuildTarget]]("List all the BSP build targets").withRank(DTask) val bspBuildTarget = settingKey[BuildTarget]("Description of the BSP build target").withRank(DSetting) val bspBuildTargetSources = inputKey[Unit]("").withRank(DTask) val bspBuildTargetSourcesItem = taskKey[SourcesItem]("").withRank(DTask) diff --git a/main/src/main/scala/sbt/Main.scala b/main/src/main/scala/sbt/Main.scala index 8aa5b32fa..23a6e5b36 100644 --- a/main/src/main/scala/sbt/Main.scala +++ b/main/src/main/scala/sbt/Main.scala @@ -242,7 +242,7 @@ object BuiltinCommands { act, continuous, clearCaches, - ) ++ allBasicCommands ++ server.BuildServerProtocol.commands + ) ++ allBasicCommands def DefaultBootCommands: Seq[String] = WriteSbtVersion :: LoadProject :: NotifyUsersAboutShell :: s"$IfLast $Shell" :: Nil diff --git a/main/src/main/scala/sbt/ScopeFilter.scala b/main/src/main/scala/sbt/ScopeFilter.scala index 8e7a4e7a1..f279afb9a 100644 --- a/main/src/main/scala/sbt/ScopeFilter.scala +++ b/main/src/main/scala/sbt/ScopeFilter.scala @@ -9,9 +9,7 @@ package sbt import sbt.internal.{ Load, LoadedBuildUnit } import sbt.internal.util.{ AttributeKey, Dag, Types } - -import sbt.librarymanagement.{ Configuration, ConfigRef } - +import sbt.librarymanagement.{ ConfigRef, Configuration } import Types.const import Def.Initialize import java.net.URI @@ -23,6 +21,16 @@ object ScopeFilter { type ConfigurationFilter = AxisFilter[ConfigKey] type TaskFilter = AxisFilter[AttributeKey[_]] + /** + * Construct a Scope filter from a sequence of individual scopes. + */ + def in(scopes: Seq[Scope]): ScopeFilter = { + val scopeSet = scopes.toSet + new ScopeFilter { + override private[ScopeFilter] def apply(data: Data): Scope => Boolean = scopeSet.contains + } + } + /** * Constructs a Scope filter from filters for the individual axes. * If a project filter is not supplied, the enclosing project is selected. diff --git a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala index a9250d36e..7f0b5bc5c 100644 --- a/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/BuildServerProtocol.scala @@ -11,32 +11,51 @@ package server import java.net.URI -import sbt.internal.bsp._ -import sbt.internal.util.complete.DefaultParsers -import sbt.librarymanagement.{ Configuration, Configurations } -import Configurations.{ Compile, Test } -import sbt.SlashSyntax0._ import sbt.BuildSyntax._ - -import scala.collection.mutable +import sbt.Def._ +import sbt.Keys._ +import sbt.ScopeFilter.Make._ +import sbt.SlashSyntax0._ +import sbt.internal.bsp._ +import sbt.internal.langserver.ErrorCodes +import sbt.internal.protocol.JsonRpcRequestMessage +import sbt.librarymanagement.Configuration +import sbt.librarymanagement.Configurations.{ Compile, Test } +import sbt.std.TaskExtra._ +import sjsonnew.shaded.scalajson.ast.unsafe.JValue import sjsonnew.support.scalajson.unsafe.Converter -import Def._ -import Keys._ -import ScopeFilter.Make._ object BuildServerProtocol { - private[sbt] val idMap: mutable.Map[BuildTargetIdentifier, (ProjectRef, Configuration)] = - mutable.Map.empty - - def commands: List[Command] = List() + import sbt.internal.bsp.codec.JsonProtocol._ + private val bspVersion = "2.0.0-M5" + private val capabilities = BuildClientCapabilities(languageIds = Vector("scala")) lazy val globalSettings: Seq[Def.Setting[_]] = Seq( + bspWorkspace := Def.taskDyn { + val structure = Keys.buildStructure.value + val scopes: Seq[Scope] = structure.allProjectRefs.flatMap { ref => + Seq(Scope.Global.in(ref, Compile), Scope.Global.in(ref, Test)) + } + scopes + .map(scope => (scope / Keys.buildTargetIdentifier).toTask) + .joinWith(tasks => joinTasks(tasks).join.map(_.zip(scopes).toMap)) + }.value, + bspWorkspaceBuildTargets := Def.taskDyn { + val workspace = Keys.bspWorkspace.value + val state = Keys.state.value + val allTargets = ScopeFilter.in(workspace.values.toSeq) + Def.task { + val buildTargets = Keys.bspBuildTarget.all(allTargets).value.toVector + state.respondEvent(WorkspaceBuildTargetsResult(buildTargets)) + buildTargets + } + }.value, // https://github.com/build-server-protocol/build-server-protocol/blob/master/docs/specification.md#build-target-sources-request - bspBuildTargetSources := (Def.inputTaskDyn { - import DefaultParsers._ + bspBuildTargetSources := Def.inputTaskDyn { val s = state.value - val args: Seq[String] = spaceDelimited("").parsed - val filter = toScopeFilter(args) + val workspace = bspWorkspace.value + val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) + val filter = ScopeFilter.in(targets.map(workspace)) // run the worker task concurrently Def.task { import sbt.internal.bsp.codec.JsonProtocol._ @@ -44,12 +63,13 @@ object BuildServerProtocol { val result = SourcesResult(items.toVector) s.respondEvent(result) } - }).evaluated, + }.evaluated, bspBuildTargetSources / aggregate := false, bspBuildTargetCompile := Def.inputTaskDyn { val s: State = state.value - val args: Seq[String] = spaceDelimited().parsed - val filter = toScopeFilter(args) + val workspace = bspWorkspace.value + val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) + val filter = ScopeFilter.in(targets.map(workspace)) Def.task { import sbt.internal.bsp.codec.JsonProtocol._ val statusCode = Keys.bspBuildTargetCompileItem.all(filter).value.max @@ -57,19 +77,17 @@ object BuildServerProtocol { } }.evaluated, bspBuildTargetCompile / aggregate := false, - bspBuildTargetScalacOptions := (Def.inputTaskDyn { - import DefaultParsers._ + bspBuildTargetScalacOptions := Def.inputTaskDyn { val s = state.value - val args: Seq[String] = spaceDelimited("").parsed - val filter = toScopeFilter(args) - // run the worker task concurrently + val workspace = bspWorkspace.value + val targets = spaceDelimited().parsed.map(uri => BuildTargetIdentifier(URI.create(uri))) + val filter = ScopeFilter.in(targets.map(workspace)) Def.task { - import sbt.internal.bsp.codec.JsonProtocol._ val items = bspBuildTargetScalacOptionsItem.all(filter).value val result = ScalacOptionsResult(items.toVector) s.respondEvent(result) } - }).evaluated, + }.evaluated, bspBuildTargetScalacOptions / aggregate := false ) @@ -86,10 +104,10 @@ object BuildServerProtocol { val dirs = unmanagedSourceDirectories.value val managed = managedSources.value val items = (dirs.toVector map { dir => - SourceItem(dir.toURI, SourceItemKind.Directory, false) + SourceItem(dir.toURI, SourceItemKind.Directory, generated = false) }) ++ (managed.toVector map { x => - SourceItem(x.toURI, SourceItemKind.File, true) + SourceItem(x.toURI, SourceItemKind.File, generated = true) }) SourcesItem(id, items) }, @@ -103,6 +121,56 @@ object BuildServerProtocol { ) ) + def handler(sbtVersion: String): ServerHandler = ServerHandler { callback => + ServerIntent( + { + case r: JsonRpcRequestMessage if r.method == "build/initialize" => + val _ = Converter.fromJson[InitializeBuildParams](json(r)).get + val response = InitializeBuildResult("sbt", sbtVersion, bspVersion, capabilities, None) + callback.jsonRpcRespond(response, Some(r.id)); () + + case r: JsonRpcRequestMessage if r.method == "workspace/buildTargets" => + val _ = callback.appendExec(Keys.bspWorkspaceBuildTargets.key.toString, None) + + case r: JsonRpcRequestMessage if r.method == "build/shutdown" => + () + + case r: JsonRpcRequestMessage if r.method == "build/exit" => + val _ = callback.appendExec("shutdown", None) + + case r: JsonRpcRequestMessage if r.method == "buildTarget/sources" => + import sbt.internal.bsp.codec.JsonProtocol._ + val param = Converter.fromJson[SourcesParams](json(r)).get + val targets = param.targets.map(_.uri).mkString(" ") + val command = Keys.bspBuildTargetSources.key + val _ = callback.appendExec(s"$command $targets", None) + + case r if r.method == "buildTarget/compile" => + val param = Converter.fromJson[CompileParams](json(r)).get + callback.log.info(param.toString) + val targets = param.targets.map(_.uri).mkString(" ") + val command = Keys.bspBuildTargetCompile.key + val _ = callback.appendExec(s"$command $targets", None) + + case r: JsonRpcRequestMessage if r.method == "buildTarget/scalacOptions" => + import sbt.internal.bsp.codec.JsonProtocol._ + val param = Converter.fromJson[ScalacOptionsParams](json(r)).get + val targets = param.targets.map(_.uri).mkString(" ") + val command = Keys.bspBuildTargetScalacOptions.key + val _ = callback.appendExec(s"$command $targets", None) + }, + PartialFunction.empty + ) + } + + private def json(r: JsonRpcRequestMessage): JValue = + r.params.getOrElse( + throw LangServerError( + ErrorCodes.InvalidParams, + s"param is expected on '${r.method}' method." + ) + ) + private def bspBuildTargetSetting: Def.Initialize[BuildTarget] = Def.settingDyn { import sbt.internal.bsp.codec.JsonProtocol._ val buildTargetIdentifier = Keys.buildTargetIdentifier.value @@ -157,13 +225,11 @@ object BuildServerProtocol { } } - def toScopeFilter(args: Seq[String]): ScopeFilter = { - val filters = args map { arg => - val id = BuildTargetIdentifier(new URI(arg)) - val pair = idMap(id) - ScopeFilter(inProjects(pair._1), inConfigurations(pair._2)) - } - filters.tail.foldLeft(filters.head) { _ || _ } + def scopeFilter( + targets: Seq[BuildTargetIdentifier], + workspace: Map[BuildTargetIdentifier, Scope] + ): ScopeFilter = { + ScopeFilter.in(targets.map(workspace)) } def toId(ref: ProjectReference, config: Configuration): BuildTargetIdentifier = @@ -191,41 +257,3 @@ object BuildServerProtocol { } yield depId } } - -// This is mixed into NetworkChannel -trait BuildServerImpl { self: LanguageServerProtocol with NetworkChannel => - protected def structure: BuildStructure - protected def getSetting[A](key: SettingKey[A]): Option[A] - protected def setting[A](key: SettingKey[A]): A - - protected def onBspInitialize(execId: Option[String], param: InitializeBuildParams): Unit = { - import sbt.internal.bsp.codec.JsonProtocol._ - val bspVersion = "2.0.0-M5" - // https://microsoft.github.io/language-server-protocol/specification#textDocumentItem - val languageIds = Vector("scala") - val sbtV = setting(Keys.sbtVersion) - val response = - InitializeBuildResult("sbt", sbtV, bspVersion, BuildClientCapabilities(languageIds), None) - respondResult(response, execId) - } - - /** - * https://github.com/build-server-protocol/build-server-protocol/blob/master/docs/specification.md#workspace-build-targets-request - */ - protected def onBspBuildTargets(execId: Option[String]): Unit = { - import sbt.internal.bsp.codec.JsonProtocol._ - val buildTargets = structure.allProjectRefs.flatMap { ref => - val compileTarget = getSetting(ref / Compile / Keys.bspBuildTarget).get - val testTarget = getSetting(ref / Test / Keys.bspBuildTarget).get - BuildServerProtocol.idMap ++= Seq( - compileTarget.id -> ((ref, Compile)), - testTarget.id -> ((ref, Test)) - ) - Seq(compileTarget, testTarget) - } - respondResult( - WorkspaceBuildTargetsResult(buildTargets.toVector), - execId - ) - } -} diff --git a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala index f7aba795e..8bcb76566 100644 --- a/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala +++ b/main/src/main/scala/sbt/internal/server/LanguageServerProtocol.scala @@ -9,25 +9,30 @@ package sbt package internal package server -import sjsonnew.JsonFormat -import sjsonnew.support.scalajson.unsafe.Converter -import sbt.protocol.Serialization -import sbt.protocol.{ CompletionParams => CP, SettingQuery => Q } -import sbt.internal.langserver.{ CancelRequestParams => CRP } -import sbt.internal.bsp._ +import sbt.internal.langserver._ import sbt.internal.protocol._ import sbt.internal.protocol.codec._ -import sbt.internal.langserver._ -import sbt.internal.util.ObjectEvent -import sbt.util.Logger -import scala.concurrent.ExecutionContext +import sbt.protocol.{ CompletionParams => CP, SettingQuery => Q } +import sjsonnew.shaded.scalajson.ast.unsafe.JValue +import sjsonnew.support.scalajson.unsafe.Converter import xsbti.FileConverter private[sbt] final case class LangServerError(code: Long, message: String) extends Throwable(message) private[sbt] object LanguageServerProtocol { - lazy val internalJsonProtocol = new InitializeOptionFormats with sjsonnew.BasicJsonProtocol {} + private val internalJsonProtocol = new sbt.internal.langserver.codec.JsonProtocol + with sbt.protocol.codec.JsonProtocol with sjsonnew.BasicJsonProtocol with InitializeOptionFormats + + import internalJsonProtocol._ + + def json(r: JsonRpcRequestMessage): JValue = + r.params.getOrElse( + throw LangServerError( + ErrorCodes.InvalidParams, + s"param is expected on '${r.method}' method." + ) + ) lazy val serverCapabilities: ServerCapabilities = { ServerCapabilities( @@ -37,200 +42,54 @@ private[sbt] object LanguageServerProtocol { ) } - def handler(converter: FileConverter): ServerHandler = - ServerHandler({ - case callback: ServerCallback => - import callback._ - ServerIntent( - { - import sbt.internal.langserver.codec.JsonProtocol._ - import internalJsonProtocol._ - import sbt.internal.bsp.codec.JsonProtocol._ - def json(r: JsonRpcRequestMessage) = - r.params.getOrElse( - throw LangServerError( - ErrorCodes.InvalidParams, - s"param is expected on '${r.method}' method." - ) + def handler(converter: FileConverter): ServerHandler = ServerHandler { callback => + import callback._ + ServerIntent( + { + case r: JsonRpcRequestMessage if r.method == "initialize" => + if (authOptions(ServerAuthentication.Token)) { + val param = Converter.fromJson[InitializeParams](json(r)).get + val optionJson = param.initializationOptions.getOrElse( + throw LangServerError( + ErrorCodes.InvalidParams, + "initializationOptions is expected on 'initialize' param." ) + ) + val opt = Converter.fromJson[InitializeOption](optionJson).get + val token = opt.token.getOrElse(sys.error("'token' is missing.")) + if (authenticate(token)) () + else throw LangServerError(ErrorCodes.InvalidRequest, "invalid token") + } else () + setInitialized(true) + appendExec("collectAnalyses", None) + jsonRpcRespond(InitializeResult(serverCapabilities), Some(r.id)) - { - case r: JsonRpcRequestMessage if r.method == "textDocument/definition" => - implicit val executionContext: ExecutionContext = StandardMain.executionContext - Definition.lspDefinition(json(r), r.id, CommandSource(name), converter, log) - () - case r: JsonRpcRequestMessage if r.method == "sbt/exec" => - val param = Converter.fromJson[SbtExecParams](json(r)).get - appendExec(Exec(param.commandLine, Some(r.id), Some(CommandSource(name)))) - () - case r: JsonRpcRequestMessage if r.method == "sbt/setting" => - import sbt.protocol.codec.JsonProtocol._ - val param = Converter.fromJson[Q](json(r)).get - onSettingQuery(Option(r.id), param) - case r: JsonRpcRequestMessage if r.method == "sbt/cancelRequest" => - import sbt.protocol.codec.JsonProtocol._ - val param = Converter.fromJson[CRP](json(r)).get - onCancellationRequest(Option(r.id), param) - case r: JsonRpcRequestMessage if r.method == "sbt/completion" => - import sbt.protocol.codec.JsonProtocol._ - val param = Converter.fromJson[CP](json(r)).get - onCompletionRequest(Option(r.id), param) - case r: JsonRpcRequestMessage if r.method == "build/initialize" => - val param = Converter.fromJson[InitializeBuildParams](json(r)).get - onBspInitialize(Option(r.id), param) - case r: JsonRpcRequestMessage if r.method == "workspace/buildTargets" => - onBspBuildTargets(Option(r.id)) - case r: JsonRpcRequestMessage if r.method == "build/shutdown" => - () - case r: JsonRpcRequestMessage if r.method == "build/exit" => - appendExec(Exec("shutdown", Some(r.id), Some(CommandSource(name)))) - () - case r: JsonRpcRequestMessage if r.method == "buildTarget/sources" => - import sbt.internal.bsp.codec.JsonProtocol._ - val param = Converter.fromJson[SourcesParams](json(r)).get - appendExec( - Exec( - s"""${Keys.bspBuildTargetSources.key} ${param.targets - .map(_.uri) - .mkString(" ")}""", - Option(r.id), - Some(CommandSource(name)) - ) - ) - () - case r if r.method == "buildTarget/compile" => - val param = Converter.fromJson[CompileParams](json(r)).get - log.info(param.toString) - val targetsUris = param.targets.map(_.uri).mkString(" ") - val key = Keys.bspBuildTargetCompile.key.toString - appendExec( - Exec( - s"$key $targetsUris", - Option(r.id), - Some(CommandSource(name)) - ) - ) - () - case r: JsonRpcRequestMessage if r.method == "buildTarget/scalacOptions" => - import sbt.internal.bsp.codec.JsonProtocol._ - val param = Converter.fromJson[ScalacOptionsParams](json(r)).get - appendExec( - Exec( - s"""${Keys.bspBuildTargetScalacOptions.key} ${param.targets - .map(_.uri) - .mkString(" ")}""", - Option(r.id), - Some(CommandSource(name)) - ) - ) - () - } - }, { - case n: JsonRpcNotificationMessage if n.method == "textDocument/didSave" => - appendExec(Exec(";Test/compile; collectAnalyses", None, Some(CommandSource(name)))) - () - } - ) - }) -} + case r: JsonRpcRequestMessage if r.method == "textDocument/definition" => + val _ = Definition.lspDefinition(json(r), r.id, CommandSource(name), converter, log)( + StandardMain.executionContext + ) -/** Implements Language Server Protocol . */ -private[sbt] trait LanguageServerProtocol { self: NetworkChannel => + case r: JsonRpcRequestMessage if r.method == "sbt/exec" => + val param = Converter.fromJson[SbtExecParams](json(r)).get + val _ = appendExec(param.commandLine, Some(r.id)) - lazy val internalJsonProtocol = new InitializeOptionFormats with sjsonnew.BasicJsonProtocol {} + case r: JsonRpcRequestMessage if r.method == "sbt/setting" => + val param = Converter.fromJson[Q](json(r)).get + onSettingQuery(Option(r.id), param) - protected def authenticate(token: String): Boolean - protected def authOptions: Set[ServerAuthentication] - protected def setInitialized(value: Boolean): Unit - protected def log: Logger - protected def onSettingQuery(execId: Option[String], req: Q): Unit - protected def onCompletionRequest(execId: Option[String], cp: CP): Unit - protected def onCancellationRequest(execId: Option[String], crp: CRP): Unit - protected def onBspInitialize(execId: Option[String], param: InitializeBuildParams): Unit - protected def onBspBuildTargets(execId: Option[String]): Unit + case r: JsonRpcRequestMessage if r.method == "sbt/cancelRequest" => + val param = Converter.fromJson[CancelRequestParams](json(r)).get + onCancellationRequest(Option(r.id), param) - protected lazy val callbackImpl: ServerCallback = new ServerCallback { - def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit = - self.respondResult(event, execId) + case r: JsonRpcRequestMessage if r.method == "sbt/completion" => + import sbt.protocol.codec.JsonProtocol._ + val param = Converter.fromJson[CP](json(r)).get + onCompletionRequest(Option(r.id), param) - def jsonRpcRespondError(execId: Option[String], code: Long, message: String): Unit = - self.respondError(code, message, execId) - - def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit = - self.jsonRpcNotify(method, params) - - def appendExec(exec: Exec): Boolean = self.append(exec) - def log: Logger = self.log - def name: String = self.name - private[sbt] def authOptions: Set[ServerAuthentication] = self.authOptions - private[sbt] def authenticate(token: String): Boolean = self.authenticate(token) - private[sbt] def setInitialized(value: Boolean): Unit = self.setInitialized(value) - private[sbt] def onSettingQuery(execId: Option[String], req: Q): Unit = - self.onSettingQuery(execId, req) - private[sbt] def onCompletionRequest(execId: Option[String], cp: CP): Unit = - self.onCompletionRequest(execId, cp) - private[sbt] def onCancellationRequest(execId: Option[String], crp: CancelRequestParams): Unit = - self.onCancellationRequest(execId, crp) - private[sbt] def onBspInitialize(execId: Option[String], param: InitializeBuildParams): Unit = - self.onBspInitialize(execId, param) - private[sbt] def onBspBuildTargets(execId: Option[String]): Unit = - self.onBspBuildTargets(execId) - } - - /** - * This reacts to various events that happens inside sbt, sometime - * in response to the previous requests. - * The type information has been erased because it went through logging. - */ - protected def onObjectEvent(event: ObjectEvent[_]): Unit = { - // import sbt.internal.langserver.codec.JsonProtocol._ - - val msgContentType = event.contentType - msgContentType match { - // LanguageServerReporter sends PublishDiagnosticsParams - case "sbt.internal.langserver.PublishDiagnosticsParams" => - // val p = event.message.asInstanceOf[PublishDiagnosticsParams] - // jsonRpcNotify("textDocument/publishDiagnostics", p) - case "xsbti.Problem" => - () // ignore - case _ => - // log.debug(event) - () - } - } - - /** Respond back to Language Server's client. */ - private[sbt] def jsonRpcRespond[A: JsonFormat](event: A, execId: String): Unit = { - val response = - JsonRpcResponseMessage("2.0", execId, Option(Converter.toJson[A](event).get), None) - val bytes = Serialization.serializeResponseMessage(response) - publishBytes(bytes) - } - - /** Respond back to Language Server's client. */ - private[sbt] def jsonRpcRespondError( - execId: String, - err: JsonRpcResponseError - ): Unit = { - val m = JsonRpcResponseMessage("2.0", execId, None, Option(err)) - val bytes = Serialization.serializeResponseMessage(m) - publishBytes(bytes) - } - - /** Notify to Language Server's client. */ - private[sbt] def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit = { - val m = - JsonRpcNotificationMessage("2.0", method, Option(Converter.toJson[A](params).get)) - log.debug(s"jsonRpcNotify: $m") - val bytes = Serialization.serializeNotificationMessage(m) - publishBytes(bytes) - } - - def logMessage(level: String, message: String): Unit = { - import sbt.internal.langserver.codec.JsonProtocol._ - jsonRpcNotify( - "window/logMessage", - LogMessageParams(MessageType.fromLevelString(level), message) + }, { + case n: JsonRpcNotificationMessage if n.method == "textDocument/didSave" => + val _ = appendExec(";Test/compile; collectAnalyses", None) + } ) } } diff --git a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala index 5adae6af2..80bb9dc19 100644 --- a/main/src/main/scala/sbt/internal/server/NetworkChannel.scala +++ b/main/src/main/scala/sbt/internal/server/NetworkChannel.scala @@ -12,15 +12,16 @@ package server import java.net.{ Socket, SocketTimeoutException } import java.util.concurrent.atomic.AtomicBoolean -import sbt.internal.langserver.{ CancelRequestParams, ErrorCodes } +import sbt.internal.langserver.{ CancelRequestParams, ErrorCodes, LogMessageParams, MessageType } import sbt.internal.protocol.{ JsonRpcNotificationMessage, JsonRpcRequestMessage, - JsonRpcResponseError + JsonRpcResponseError, + JsonRpcResponseMessage } +import sbt.internal.util.ObjectEvent import sbt.internal.util.codec.JValueFormats import sbt.internal.util.complete.Parser -import sbt.internal.util.ObjectEvent import sbt.protocol._ import sbt.util.Logger import sjsonnew._ @@ -39,9 +40,7 @@ final class NetworkChannel( instance: ServerInstance, handlers: Seq[ServerHandler], val log: Logger -) extends CommandChannel - with LanguageServerProtocol - with BuildServerImpl { +) extends CommandChannel { self => import NetworkChannel._ private val running = new AtomicBoolean(true) @@ -59,6 +58,34 @@ final class NetworkChannel( private lazy val jsonFormat = new sjsonnew.BasicJsonProtocol with JValueFormats {} private val pendingRequests: mutable.Map[String, JsonRpcRequestMessage] = mutable.Map() + private lazy val callback: ServerCallback = new ServerCallback { + def jsonRpcRespond[A: JsonFormat](event: A, execId: Option[String]): Unit = + self.respondResult(event, execId) + + def jsonRpcRespondError(execId: Option[String], code: Long, message: String): Unit = + self.respondError(code, message, execId) + + def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit = + self.jsonRpcNotify(method, params) + + def appendExec(commandLine: String, execId: Option[String]): Boolean = + self.append(Exec(commandLine, execId, Some(CommandSource(name)))) + + def appendExec(exec: Exec): Boolean = self.append(exec) + + def log: Logger = self.log + def name: String = self.name + private[sbt] def authOptions: Set[ServerAuthentication] = self.authOptions + private[sbt] def authenticate(token: String): Boolean = self.authenticate(token) + private[sbt] def setInitialized(value: Boolean): Unit = self.setInitialized(value) + private[sbt] def onSettingQuery(execId: Option[String], req: SettingQuery): Unit = + self.onSettingQuery(execId, req) + private[sbt] def onCompletionRequest(execId: Option[String], cp: CompletionParams): Unit = + self.onCompletionRequest(execId, cp) + private[sbt] def onCancellationRequest(execId: Option[String], crp: CancelRequestParams): Unit = + self.onCancellationRequest(execId, crp) + } + def setContentType(ct: String): Unit = synchronized { _contentType = ct } def contentType: String = _contentType @@ -172,11 +199,11 @@ final class NetworkChannel( } private lazy val intents = { - val cb = callbackImpl handlers.toVector map { h => - h.handler(cb) + h.handler(callback) } } + lazy val onRequestMessage: PartialFunction[JsonRpcRequestMessage, Unit] = intents.foldLeft(PartialFunction.empty[JsonRpcRequestMessage, Unit]) { case (f, i) => f orElse i.onRequest @@ -396,18 +423,6 @@ final class NetworkChannel( } } - protected def getSetting[A](key: SettingKey[A]): Option[A] = - structure.data.get(key.scope, key.key) - - protected def getTaskValue[A](key: TaskKey[A]): Option[Task[A]] = - structure.data.get(key.scope, key.key) - - protected def setting[A](key: SettingKey[A]): A = - getSetting(key).getOrElse(sys.error(s"key ${Def.displayFull(key.scopedKey)} is not defined")) - - protected def taskValue[A](key: TaskKey[A]): Task[A] = - getTaskValue(key).getOrElse(sys.error(s"key ${Def.displayFull(key.scopedKey)} is not defined")) - private def onExecCommand(cmd: ExecCommand) = { if (initialized) { append( @@ -535,6 +550,73 @@ final class NetworkChannel( running.set(false) out.close() } + + /** + * This reacts to various events that happens inside sbt, sometime + * in response to the previous requests. + * The type information has been erased because it went through logging. + */ + protected def onObjectEvent(event: ObjectEvent[_]): Unit = { + // import sbt.internal.langserver.codec.JsonProtocol._ + + val msgContentType = event.contentType + msgContentType match { + // LanguageServerReporter sends PublishDiagnosticsParams + case "sbt.internal.langserver.PublishDiagnosticsParams" => + // val p = event.message.asInstanceOf[PublishDiagnosticsParams] + // jsonRpcNotify("textDocument/publishDiagnostics", p) + case "xsbti.Problem" => + () // ignore + case _ => + // log.debug(event) + () + } + } + + /** Respond back to Language Server's client. */ + private[sbt] def jsonRpcRespond[A: JsonFormat](event: A, execId: String): Unit = { + val m = + JsonRpcResponseMessage("2.0", execId, Option(Converter.toJson[A](event).get), None) + val bytes = Serialization.serializeResponseMessage(m) + publishBytes(bytes) + } + + /** Respond back to Language Server's client. */ + private[sbt] def jsonRpcRespondError[A: JsonFormat]( + execId: String, + code: Long, + message: String, + data: A, + ): Unit = { + val err = JsonRpcResponseError(code, message, Converter.toJson[A](data).get) + jsonRpcRespondError(execId, err) + } + + private[sbt] def jsonRpcRespondError( + execId: String, + err: JsonRpcResponseError + ): Unit = { + val m = JsonRpcResponseMessage("2.0", execId, None, Option(err)) + val bytes = Serialization.serializeResponseMessage(m) + publishBytes(bytes) + } + + /** Notify to Language Server's client. */ + private[sbt] def jsonRpcNotify[A: JsonFormat](method: String, params: A): Unit = { + val m = + JsonRpcNotificationMessage("2.0", method, Option(Converter.toJson[A](params).get)) + log.debug(s"jsonRpcNotify: $m") + val bytes = Serialization.serializeNotificationMessage(m) + publishBytes(bytes) + } + + def logMessage(level: String, message: String): Unit = { + import sbt.internal.langserver.codec.JsonProtocol._ + jsonRpcNotify( + "window/logMessage", + LogMessageParams(MessageType.fromLevelString(level), message) + ) + } } object NetworkChannel {