diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a07e7b4d4..ef10fa4a6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,6 @@ jobs: run: sbt ci buildWebsite - name: Publish ${{ github.ref }} - if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main')) run: sbt ci-release env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} diff --git a/build.sbt b/build.sbt index 4d3cb5071..be2fda93d 100644 --- a/build.sbt +++ b/build.sbt @@ -21,7 +21,8 @@ inThisBuild( "keynmol@gmail.com", url("https://blog.indoorvivants.com") ) - ) + ), + resolvers ++= Resolver.sonatypeOssRepos("snapshots") ) ) @@ -30,7 +31,7 @@ val V = new { val scribe = "3.13.2" val upickle = "2.0.0" val cats = "2.10.0" - val jsonrpclib = "0.0.7" + val jsonrpclib = "0.1.0" val fs2 = "3.10.0" val http4s = "0.23.26" val laminar = "0.14.5" @@ -147,7 +148,7 @@ lazy val app = projectMatrix libraryDependencies += "tech.neander" %%% "jsonrpclib-fs2" % V.jsonrpclib, libraryDependencies += "co.fs2" %%% "fs2-io" % V.fs2, libraryDependencies += "com.outr" %%% "scribe-cats" % V.scribe, - test := {}, + test := {}, scalacOptions ++= commonScalacOptions ) .jvmPlatform(V.scalaVersions) @@ -255,12 +256,16 @@ lazy val tracer = projectMatrix .settings( name := "langoustine-tracer", libraryDependencies += "tech.neander" %%% "jsonrpclib-fs2" % V.jsonrpclib, - libraryDependencies += "co.fs2" %%% "fs2-io" % V.fs2, + libraryDependencies += "co.fs2" %%% "fs2-io" % V.fs2, libraryDependencies += "org.http4s" %%% "http4s-ember-server" % V.http4s, libraryDependencies += "org.http4s" %%% "http4s-dsl" % V.http4s, libraryDependencies += "com.monovore" %%% "decline" % V.decline, libraryDependencies += "com.outr" %%% "scribe-cats" % V.scribe, libraryDependencies += "com.indoorvivants.detective" %% "platform" % V.detective, + libraryDependencies ++= Seq( + // Use the "provided" scope instead when the "compile-internal" scope is not supported + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % V.jsoniter % "compile-internal" + ), scalacOptions ++= commonScalacOptions, Compile / doc / sources := Seq.empty, // embedding frontend in backend's resources @@ -322,7 +327,11 @@ lazy val tracerFrontend = projectMatrix libraryDependencies += "com.raquo" %%% "laminar" % V.laminar, libraryDependencies += "io.circe" %%% "circe-scalajs" % V.circe, libraryDependencies += "com.lihaoyi" %%% "fansi" % V.fansi, - scalaJSUseMainModuleInitializer := true, + libraryDependencies ++= Seq( + // Use the "provided" scope instead when the "compile-internal" scope is not supported + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % "2.35.3" % "compile-internal" + ), + scalaJSUseMainModuleInitializer := true, scalacOptions ++= commonScalacOptions ) .jsPlatform(V.scalaVersions) @@ -335,6 +344,7 @@ lazy val tracerShared = projectMatrix name := "langoustine-tracer-shared", libraryDependencies ++= Seq( "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % V.jsoniter, + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-circe" % V.jsoniter, "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % V.jsoniter % "compile-internal", "tech.neander" %%% "jsonrpclib-core" % V.jsonrpclib ), diff --git a/modules/app/src/main/scala/LSPCancelRequest.scala b/modules/app/src/main/scala/LSPCancelRequest.scala index fb6317abb..739187638 100644 --- a/modules/app/src/main/scala/LSPCancelRequest.scala +++ b/modules/app/src/main/scala/LSPCancelRequest.scala @@ -16,17 +16,15 @@ package langoustine.lsp.app +import io.circe.Codec import jsonrpclib.CallId -import jsonrpclib.Codec -import com.github.plokhotnyuk.jsoniter_scala.core.JsonValueCodec import jsonrpclib.fs2.CancelTemplate private case class LSPCancelRequest(id: CallId) object LSPCancelRequest: - import com.github.plokhotnyuk.jsoniter_scala.macros.* - given JsonValueCodec[LSPCancelRequest] = JsonCodecMaker.make[LSPCancelRequest] - given Codec[LSPCancelRequest] = Codec.fromJsonCodec + given Codec[LSPCancelRequest] = io.circe.Codec + .forProduct1[LSPCancelRequest, CallId]("id")(LSPCancelRequest(_))(_.id) val cancelTemplate: CancelTemplate = CancelTemplate .make[LSPCancelRequest]( diff --git a/modules/app/src/main/scala/LangoustineApp.scala b/modules/app/src/main/scala/LangoustineApp.scala index a0ede6b7c..706991f48 100644 --- a/modules/app/src/main/scala/LangoustineApp.scala +++ b/modules/app/src/main/scala/LangoustineApp.scala @@ -26,9 +26,6 @@ import cats.effect.kernel.Resource import cats.effect.std.Dispatcher import cats.syntax.all.* import jsonrpclib.Channel -import jsonrpclib.Endpoint -import jsonrpclib.Endpoint.NotificationEndpoint -import jsonrpclib.Endpoint.RequestResponseEndpoint import jsonrpclib.fs2.* import langoustine.lsp.Communicate import langoustine.lsp.LSPBuilder @@ -76,18 +73,13 @@ object LangoustineApp: val futureComms = DispatcherCommunicate(disp, comms) val endpoints = builder.build(futureComms) - (endpoints: @unchecked) + (endpoints) .map { - case n @ NotificationEndpoint[Future, Any](method, run, inCodec) => - n.copy(run = (msg, in) => IO.fromFuture(IO(n.run(msg, in)))) - case r @ RequestResponseEndpoint[Future, Any, Any, Any]( - method, - run, - inCodec, - errCodec, - outCodec - ) => - r.copy(run = (msg, in) => IO.fromFuture(IO(r.run(msg, in)))) + _.mapK( + new jsonrpclib.PolyFunction[Future, IO]: + def apply[A0](fa: => Future[A0]): IO[A0] = + IO.fromFuture(IO(fa)) + ) } .traverse(ep => to.mountEndpoint(ep)) }.void @@ -131,13 +123,15 @@ object LangoustineApp: fs2.Stream .eval(IO.deferred[Boolean]) .flatMap { latch => - FS2Channel[IO](bufferSize, Some(LSPCancelRequest.cancelTemplate)) + FS2Channel + .stream[IO](bufferSize, Some(LSPCancelRequest.cancelTemplate)) .flatMap { channel => (builder: @unchecked) match case l: LSPBuilder[IO] => - FS2.Stream.resource( - Resource.eval(l.bind(channel, latch.complete(true).void)) - ) + FS2.Stream + .resource( + Resource.eval(l.bind(channel, latch.complete(true).void)) + ) case other: LSPServerBuilder[IO] => FS2.Stream .resource(other(channel, latch.complete(true).void)) diff --git a/modules/e2e-tests/src/test/scala/EndToEndTests.scala b/modules/e2e-tests/src/test/scala/EndToEndTests.scala index 0a1cc1f07..029fb8e37 100644 --- a/modules/e2e-tests/src/test/scala/EndToEndTests.scala +++ b/modules/e2e-tests/src/test/scala/EndToEndTests.scala @@ -16,27 +16,16 @@ package tests.e2e +import cats.effect.IO +import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.* +import com.github.plokhotnyuk.jsoniter_scala.core.* +import jsonrpclib.Payload +import langoustine.example.MyCustomNotification +import langoustine.example.MyCustomRequest import langoustine.lsp.all.* - import weaver.* -import com.github.plokhotnyuk.jsoniter_scala.core.* -import upickle.default.ReadWriter -import cats.effect.IO -import scala.sys.process.ProcessIO import concurrent.duration.* -import scala.sys.process.BasicIO -import jsonrpclib.Payload - -import cats.syntax.all.* -import scala.concurrent.Promise -import cats.effect.std.* -import cats.effect.kernel.Ref -import cats.effect.kernel.Deferred -import cats.effect.kernel.Resource -import java.lang.management.ManagementFactory - -import langoustine.example.{MyCustomRequest, MyCustomNotification} case class Result(code: Int, stdout: List[String], stderr: List[String]) @@ -166,7 +155,7 @@ transparent inline def asNotification[T <: LSPNotification]( p: Payload ): Option[t.In] = for - js <- scala.util.Try(ujson.read(writeToArray(p))).toOption + js <- scala.util.Try(ujson.read(writeToArray(p.data))).toOption o <- js.objOpt p <- o.get("params") res <- scala.util.Try(upickle.default.read[t.In](p)).toOption @@ -179,7 +168,7 @@ transparent inline def asResponse[T <: LSPRequest]( p: Payload ): Option[t.Out] = for - js <- scala.util.Try(ujson.read(writeToArray(p))).toOption + js <- scala.util.Try(ujson.read(writeToArray(p.data))).toOption o <- js.objOpt p <- o.get("result") res <- scala.util.Try(upickle.default.read[t.Out](p)).toOption diff --git a/modules/lsp/src/main/scala/JSONRPC.scala b/modules/lsp/src/main/scala/JSONRPC.scala index f0422dee9..0bffabe9a 100644 --- a/modules/lsp/src/main/scala/JSONRPC.scala +++ b/modules/lsp/src/main/scala/JSONRPC.scala @@ -23,31 +23,42 @@ import upickle.core.TraceVisitor.TraceException import upickle.default.* import requests.* +import io.circe.Codec +import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.* +import com.github.plokhotnyuk.jsoniter_scala.core.readFromString +import com.github.plokhotnyuk.jsoniter_scala.core.writeToString +import io.circe.Json +import io.circe.HCursor +import io.circe.Decoder.Result +import io.circe.DecodingFailure -private[langoustine] object jsonrpcIntegration: +object jsonrpcIntegration: val nullArray = "null".getBytes() given codec[T: Reader: Writer]: Codec[T] = new Codec[T]: - override def decode( - payload: Option[Payload] - ): Either[ProtocolError, T] = - payload - .map(_.stripNull.map(_.array).getOrElse(nullArray)) - .toRight(ProtocolError.InvalidParams("missing payload")) - .flatMap: arr => - Try(read[T](arr, trace = true)).toEither.left.map { - case te: TraceException => - val e = te.getCause() - ProtocolError.InvalidParams( - s"invalid payload at ${te.jsonPath}: " + e.getMessage - ) - - case e => - ProtocolError.InternalError("oopsie daisy: " + e.getMessage) - } - - override def encode(a: T): Payload = - Payload(write(a).getBytes) + + override def apply(c: HCursor): Result[T] = + Try(read[T](writeToString[Json](c.value))).toEither.left.map { + case te: TraceException => + val e = te.getCause() + DecodingFailure( + DecodingFailure.Reason.CustomReason( + s"invalid payload at ${te.jsonPath}: " + e.getMessage + ), + c + ) + + case e => + DecodingFailure( + DecodingFailure.Reason.CustomReason( + "oopsie daisy: " + e.getMessage + ), + c + ) + } + + override def apply(a: T): Json = + readFromString[Json](write(a)) def handlerToEndpoint[F[_]: Monadic, T <: LSPRequest](req: T)( f: req.In => F[req.Out] diff --git a/modules/tests/src/test/scala/CodecTest.scala b/modules/tests/src/test/scala/CodecTest.scala index 3f9a322cf..1545d4258 100644 --- a/modules/tests/src/test/scala/CodecTest.scala +++ b/modules/tests/src/test/scala/CodecTest.scala @@ -18,14 +18,9 @@ package tests.core import upickle.default.* import langoustine.lsp.* -import jsonrpclib.Monadic -import scala.util.* -import cats.MonadThrow import langoustine.lsp.all.* -import cats.Monad -import jsonrpclib.* import org.scalacheck.* import org.scalacheck.rng.Seed diff --git a/modules/tests/src/test/scala/LSPTests.scala b/modules/tests/src/test/scala/LSPTests.scala index 0b7c3b621..09a1d14e2 100644 --- a/modules/tests/src/test/scala/LSPTests.scala +++ b/modules/tests/src/test/scala/LSPTests.scala @@ -18,18 +18,28 @@ package tests.core import langoustine.lsp.* import jsonrpclib.Monadic +import jsonrpclib.fs2.catsMonadic import scala.util.* import langoustine.lsp.all.* +import fs2.Stream +import jsonrpclib.* +import cats.effect.IO +import scala.concurrent.duration.* + +import weaver.* +import _root_.fs2.concurrent.SignallingRef + def basicServer[F[_]: Monadic] = LSPBuilder.create[F] -import jsonrpclib.* +object LSPTests extends SimpleIOSuite: -object LSPTests extends weaver.FunSuite: - test("initialize") { + def testStream(name: TestName)(run: Stream[IO, Expectations]): Unit = + test(name)(run.compile.lastOrError.timeout(10.second)) + testStream("initialize") { import requests.* val capabilities = @@ -39,56 +49,67 @@ object LSPTests extends weaver.FunSuite: Opt(DocumentSymbolOptions(label = Opt("howdy"))) ) - val server = basicServer[Try].handleRequest(initialize) { in => - Try { - InitializeResult(capabilities) - } - } - - val (response, _) = request( - server, - CallId.StringId("resp1"), - initialize, - InitializeParams( - processId = Opt(25), - rootUri = Opt(DocumentUri("/howdy")), - capabilities = ClientCapabilities() + for + channel <- setupChannels( + mkServer = channel => + basicServer[IO] + .handleRequest(initialize) { in => + IO(InitializeResult(capabilities)) + } + .build(Communicate.channel(channel, IO.unit)) ) - ).get - - expect.same(response.capabilities, capabilities) + response <- request( + channel, + initialize, + InitializeParams( + processId = Opt(25), + rootUri = Opt(DocumentUri("/howdy")), + capabilities = ClientCapabilities() + ) + ) + yield expect.same(response.capabilities, capabilities) + end for } - test("didOpen") { + testStream("didOpen") { import requests.* - val server = basicServer[Try].handleNotification(textDocument.didOpen) { - in => - in.toClient.notification( - window.showMessage, - ShowMessageParams( - MessageType.Info, - s"you opened a ${in.params.textDocument.languageId} document from ${in.params.textDocument.uri}!" + for + ref <- Stream.eval( + SignallingRef[IO, Map[LSPNotification, List[Any]]](Map.empty) + ) + channel <- setupChannels( + mkServer = channel => + basicServer[IO] + .handleNotification(textDocument.didOpen) { in => + in.toClient.notification( + window.showMessage, + ShowMessageParams( + MessageType.Info, + s"you opened a ${in.params.textDocument.languageId} document from ${in.params.textDocument.uri}!" + ) + ) + } + .build(Communicate.channel(channel, IO.unit)), + mkClient = + _ => List(collectNotificationEndpoint(ref)(window.showMessage)) + ) + _ <- notification( + channel, + textDocument.didOpen, + DidOpenTextDocumentParams( + TextDocumentItem( + uri = DocumentUri("/home/bla.txt"), + languageId = "text", + version = 0, + text = "Hello!" ) ) - } - - val back = notification( - server, - textDocument.didOpen, - DidOpenTextDocumentParams( - TextDocumentItem( - uri = DocumentUri("/home/bla.txt"), - languageId = "text", - version = 0, - text = "Hello!" - ) ) - ).get - - expect.same( - back.collect(window.showMessage).get, + response <- getCaputured(ref.discrete)(window.showMessage) + yield expect.same( + response, List( ShowMessageParams( MessageType.Info, @@ -96,10 +117,10 @@ object LSPTests extends weaver.FunSuite: ) ) ) - + end for } - test("textDocument/documentSymbol") { + testStream("textDocument/documentSymbol") { import requests.* val symbols: Opt[Vector[DocumentSymbol]] = @@ -124,25 +145,25 @@ object LSPTests extends weaver.FunSuite: ) ) - val server = basicServer[Try].handleRequest(textDocument.documentSymbol) { - in => - Try { - symbols - } - } - - val (response, _) = request( - server, - CallId.StringId("resp1"), - textDocument.documentSymbol, - DocumentSymbolParams( - TextDocumentIdentifier(DocumentUri("/home/bla.txt")) + for + channel <- setupChannels( + mkServer = channel => + basicServer[IO] + .handleRequest(textDocument.documentSymbol) { _ => + IO(symbols) + } + .build(Communicate.channel(channel, IO.unit)) ) - ).get - expect.same( - response, - symbols - ) + response <- request( + channel, + textDocument.documentSymbol, + DocumentSymbolParams( + TextDocumentIdentifier(DocumentUri("/home/bla.txt")) + ) + ) + yield expect.same(response, symbols) + end for + } end LSPTests diff --git a/modules/tests/src/test/scala/testkit.scala b/modules/tests/src/test/scala/testkit.scala index 730e75914..71698f371 100644 --- a/modules/tests/src/test/scala/testkit.scala +++ b/modules/tests/src/test/scala/testkit.scala @@ -16,156 +16,75 @@ package tests.core +import fs2.Stream +import jsonrpclib.fs2.FS2Channel import langoustine.lsp.* import jsonrpclib.* import cats.MonadThrow -import scala.util.* -import scala.annotation.tailrec -import java.util.concurrent.atomic.AtomicReference -import jsonrpclib.InputMessage.RequestMessage -import jsonrpclib.InputMessage.NotificationMessage -import com.github.plokhotnyuk.jsoniter_scala.core.writeToArray - -given [F[_]](using MonadThrow[F]): Monadic[F] with - def doFlatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = - MonadThrow[F].flatMap(fa)(f) - - def doPure[A](a: A): F[A] = MonadThrow[F].pure(a) - - def doAttempt[A](fa: F[A]): F[Either[Throwable, A]] = - MonadThrow[F].attempt(fa) - - def doRaiseError[A](e: Throwable): F[A] = MonadThrow[F].raiseError(e) -end given - -trait RefLike[F[_], A]: - def get: F[A] - def update(f: A => A): F[Unit] - def set(other: A): F[Unit] = update(_ => other) - -trait RefConstructor[F[_]]: - def apply[A](a: A): F[RefLike[F, A]] - -object RefLike: - def apply[F[_]: RefConstructor]: Constructor[F] = new Constructor[F] - - class Constructor[F[_]: RefConstructor]: - def apply[A](a: A): F[RefLike[F, A]] = summon[RefConstructor[F]].apply(a) - -import cats.syntax.all.* - -import requests.* - -case class CollectNotifications[F[_]: MonadThrow] private ( - sent: RefLike[F, Map[LSPNotification, List[Any]]] -) extends Communicate[F]: - def notification[X <: LSPNotification](notif: X, in: notif.In): F[Unit] = - sent.update { mp => - mp.get(notif) match - case None => mp.updated(notif, List(in)) - case Some(lst) => mp.updated(notif, lst :+ in) - } - - def request[X <: LSPRequest](req: X, in: req.In): F[req.Out] = - Communicate.drop[F].request(req, in) - - def shutdown = ??? - - def collect[T <: LSPNotification](req: T): F[List[req.In]] = - sent.get.map(_.get(req).toList.flatten.map(_.asInstanceOf[req.In])) -end CollectNotifications - -object CollectNotifications: - def create[F[_]: MonadThrow: RefConstructor]: F[CollectNotifications[F]] = - RefLike[F].apply(Map.empty[LSPNotification, List[Any]]).map { rf => - new CollectNotifications[F](rf) - } +import langoustine.lsp.jsonrpcIntegration.given +import cats.effect.IO +import cats.effect.kernel.Ref + +def setupChannels[Alg[_[_, _, _, _, _]]]( + mkServer: FS2Channel[IO] => List[Endpoint[IO]], + mkClient: FS2Channel[IO] => List[Endpoint[IO]] = _ => List.empty +): Stream[IO, Channel[IO]] = + for + serverSideChannel <- FS2Channel.stream[IO]() + clientSideChannel <- FS2Channel.stream[IO]() + serverChannelWithEndpoints <- serverSideChannel.withEndpointsStream( + mkServer(serverSideChannel) + ) + clientChannelWithEndpoints <- clientSideChannel.withEndpointsStream( + mkClient(clientSideChannel) + ) + _ <- Stream(()) + .concurrently( + clientChannelWithEndpoints.output + .through(serverChannelWithEndpoints.input) + ) + .concurrently( + serverChannelWithEndpoints.output + .through(clientChannelWithEndpoints.input) + ) + yield clientChannelWithEndpoints -def request[F[_]: RefConstructor: MonadThrow, T <: requests.LSPRequest]( - builder: LSPBuilder[F], - id: CallId, +def request[F[_]: MonadThrow, T <: requests.LSPRequest]( + channel: Channel[F], req: T, in: req.In ) = - val F = MonadThrow[F] - CollectNotifications.create[F].flatMap { communicate => - builder - .build(communicate) - .find(_.method == req.requestMethod) - .collectFirst { - case Endpoint.RequestResponseEndpoint(_, run, inc, erc, outc) => - val encoded = Payload(upickle.default.write(in).getBytes()) - for - lifted <- F.fromEither(inc.decode(Some(encoded))) - msg = RequestMessage( - method = req.requestMethod, - callId = id, - params = Some(encoded) - ) - res <- run(msg, lifted).flatMap { - case Left(err) => F.raiseError(erc.encode(err)) - case Right(res) => - F.catchNonFatal { - upickle.default.read[req.Out](writeToArray(outc.encode(res))) - } - } - yield res -> communicate - end for - } - .getOrElse( - F.raiseError( - new Throwable(s"Method ${req.requestMethod} is not handled") - ) - ) - } + val remoteCall = channel.simpleStub[req.In, req.Out](req.requestMethod) + Stream.eval(remoteCall(in)) end request -def notification[F[ - _ -]: RefConstructor: MonadThrow, T <: requests.LSPNotification]( - builder: LSPBuilder[F], +def notification[F[_]: MonadThrow, T <: requests.LSPNotification]( + channel: Channel[F], req: T, in: req.In -): F[CollectNotifications[F]] = - val F = MonadThrow[F] - CollectNotifications.create[F].flatMap { communicate => - builder - .build(communicate) - .find(_.method == req.notificationMethod) - .collectFirst { case Endpoint.NotificationEndpoint(_, run, inc) => - val encoded = Payload(upickle.default.write(in).getBytes()) - for - lifted <- F.fromEither(inc.decode(Some(encoded))) - msg = NotificationMessage( - method = req.notificationMethod, - params = Some(encoded) - ) - _ <- run(msg, lifted) - yield communicate - end for - } - .getOrElse( - F.raiseError( - new Throwable(s"Method ${req.notificationMethod} is not handled") - ) - ) - } +) = + val remoteCall = channel.notificationStub[req.In](req.notificationMethod) + Stream.eval(remoteCall(in)) end notification -given RefConstructor[Try] with - def apply[A](a: A): Try[RefLike[scala.util.Try, A]] = Try { - new RefLike[Try, A]: - var ar = new AtomicReference(a) - override def get: Try[A] = Try(ar.get()) - override def update(f: A => A): Try[Unit] = - @tailrec - def spin(): Unit = - val a = ar.get - val u = f(a) - if !ar.compareAndSet(a, u) then spin() - Try(spin()) - - } -end given +def collectNotificationEndpoint[F[_], T <: requests.LSPNotification]( + ref: Ref[F, Map[requests.LSPNotification, List[Any]]] +)(req: T) = + Endpoint[F](req.notificationMethod) + .notification[req.In](in => + ref.update { mp => + mp.updatedWith(req) { prev => + prev.map(_ :+ in).orElse(Some(List(in))) + } + } + ) + +def getCaputured[F[_], T <: requests.LSPNotification]( + ref: Stream[F, Map[requests.LSPNotification, List[Any]]] +)(req: T) = + ref + .dropWhile(_.isEmpty) + .map(_.get(req)) + .collectFirst { case Some(v) => v.asInstanceOf[List[req.In]] } diff --git a/modules/tests/src/test/scalajvm/tracer/Feed.scala b/modules/tests/src/test/scalajvm/tracer/Feed.scala index d67bbde9b..323284a5b 100644 --- a/modules/tests/src/test/scalajvm/tracer/Feed.scala +++ b/modules/tests/src/test/scalajvm/tracer/Feed.scala @@ -16,21 +16,14 @@ package tests.tracer -import weaver.* import cats.effect.* import jsonrpclib.* -import java.util.Base64 import langoustine.tracer.RawMessage -import _root_.fs2.concurrent.Channel as Chan import _root_.fs2.* import cats.syntax.all.* import langoustine.tracer.* import com.github.plokhotnyuk.jsoniter_scala.core.* -import org.http4s.client.* -import TracerServer.{*, given} -import org.http4s.Uri -import org.http4s.client.websocket.* import _root_.fs2.concurrent.Topic case class Feed( diff --git a/modules/tests/src/test/scalajvm/tracer/Front.scala b/modules/tests/src/test/scalajvm/tracer/Front.scala index 5d6150a36..09028a801 100644 --- a/modules/tests/src/test/scalajvm/tracer/Front.scala +++ b/modules/tests/src/test/scalajvm/tracer/Front.scala @@ -16,18 +16,12 @@ package tests.tracer -import weaver.* import cats.effect.* import jsonrpclib.* -import java.util.Base64 import langoustine.tracer.RawMessage -import _root_.fs2.concurrent.Channel as Chan -import _root_.fs2.* import cats.syntax.all.* import langoustine.tracer.* -import com.github.plokhotnyuk.jsoniter_scala.core.* import org.http4s.client.* -import TracerServer.{*, given} import org.http4s.Uri import org.http4s.client.websocket.* import langoustine.tracer.codecs.given @@ -77,4 +71,5 @@ case class Front(client: Client[IO], base: Uri, ws: WSClient[IO]): private def cid(c: CallId) = c match case CallId.NumberId(n) => n.toString case CallId.StringId(s) => s + case CallId.NullId => ??? end Front diff --git a/modules/tests/src/test/scalajvm/tracer/ServerSpec.scala b/modules/tests/src/test/scalajvm/tracer/ServerSpec.scala index 29af9332e..5814b862f 100644 --- a/modules/tests/src/test/scalajvm/tracer/ServerSpec.scala +++ b/modules/tests/src/test/scalajvm/tracer/ServerSpec.scala @@ -17,8 +17,6 @@ package tests.tracer import cats.effect.* -import cats.effect.syntax.all.* -import jsonrpclib.Payload import fs2.concurrent.Topic import cats.syntax.all.* import fs2.Chunk diff --git a/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala b/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala index a723fd1bd..db29ba5a5 100644 --- a/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala +++ b/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala @@ -17,15 +17,13 @@ package tests.tracer import langoustine.tracer.* -import TracerServer.{*, given} +import TracerServer.* import cats.effect.* -import cats.syntax.all.* import com.github.plokhotnyuk.jsoniter_scala.core.* -import java.util.Base64 -import org.http4s.Uri import org.http4s.client.* import org.http4s.client.websocket.* import scala.concurrent.duration.* +import io.circe.Json import weaver.* import jsonrpclib.Payload @@ -64,7 +62,7 @@ object TracerServerSpec extends ServerSpec: clientResp1 = RawMessage.create( id = id, - result = Some(Payload("{}".getBytes())) + result = Some(Payload(Json.obj())) ) _ <- serv.send(_.in, clientResp1) @@ -82,10 +80,10 @@ object TracerServerSpec extends ServerSpec: test("Records outgoing responses") { serv => for msg1 <- serv.genId.map(id => - RawMessage.create(id = id, result = Some(Payload("{}".getBytes()))) + RawMessage.create(id = id, result = Some(Payload(Json.obj()))) ) msg2 <- serv.genId.map(id => - RawMessage.create(id = id, result = Some(Payload("{}".getBytes()))) + RawMessage.create(id = id, result = Some(Payload(Json.obj()))) ) _ <- serv.send(_.out, msg1) diff --git a/modules/tracer/backend/src/main/scala/command.trace.scala b/modules/tracer/backend/src/main/scala/command.trace.scala index eb4780497..8f9e36a6d 100644 --- a/modules/tracer/backend/src/main/scala/command.trace.scala +++ b/modules/tracer/backend/src/main/scala/command.trace.scala @@ -30,7 +30,7 @@ extension (s: fs2.Stream[IO, Payload]) def debugAs(name: String) = s.evalTap(el => Logging.debug( - s"[$name (payload)]: ${el.stripNull.map(a => new String(a.array)).getOrElse("null")}" + s"[$name (payload)]: ${el.stripNull.getOrElse("null")}" ) ) @@ -69,7 +69,7 @@ def Trace( val outPayloads = outBytes.subscribe(1024) val errStream = errBytes.subscribe(1024) - val channel = FS2Channel[IO](2048, None).flatMap { rpcChannel => + val channel = FS2Channel.stream[IO](2048, None).flatMap { rpcChannel => val communicate = Communicate.channel(rpcChannel, IO.unit) val readIn = inPayloads diff --git a/modules/tracer/backend/src/main/scala/state.scala b/modules/tracer/backend/src/main/scala/state.scala index 38d9a1819..fa27aaec0 100644 --- a/modules/tracer/backend/src/main/scala/state.scala +++ b/modules/tracer/backend/src/main/scala/state.scala @@ -20,7 +20,6 @@ import cats.effect.* import cats.effect.std.* import fs2.concurrent.* import jsonrpclib.CallId -import jsonrpclib.Codec import jsonrpclib.InputMessage.NotificationMessage import jsonrpclib.InputMessage.RequestMessage import jsonrpclib.Message @@ -29,6 +28,7 @@ import jsonrpclib.OutputMessage.ResponseMessage import jsonrpclib.Payload import langoustine.lsp.enumerations import langoustine.lsp.requests.window +import io.circe.{Codec, HCursor} case class State( ch: Channel[IO, String], @@ -111,9 +111,13 @@ case class State( if notif.method == "window/logMessage" then IO .fromEither( - Codec.decode[window.logMessage.In]( - rawMessage.value.params - ) + rawMessage.value.params + .map(_.data) + .toRight(new RuntimeException("missing payload")) + .flatMap { payload => + Codec[window.logMessage.In] + .apply(HCursor.fromJson(payload)) + } ) .flatMap(Received.capture(_)) .flatMap { case Received(ts, lmp) => diff --git a/modules/tracer/frontend/src/main/scala/component.jsonviewer.scala b/modules/tracer/frontend/src/main/scala/component.jsonviewer.scala index 28b1f6fd2..3073e217b 100644 --- a/modules/tracer/frontend/src/main/scala/component.jsonviewer.scala +++ b/modules/tracer/frontend/src/main/scala/component.jsonviewer.scala @@ -16,10 +16,6 @@ package langoustine.tracer -import scala.scalajs.js.JSON - -import com.github.plokhotnyuk.jsoniter_scala.core.* -import com.github.plokhotnyuk.jsoniter_scala.macros.JsonCodecMaker import com.raquo.laminar.api.L.* import jsonrpclib.* @@ -60,15 +56,21 @@ def jsonViewer( def displayErr(ep: ErrorPayload) = div( bar(b(color := "pink", "Error")), - displayJson(ep, mode.signal, modalBus) + displayJson( + io.circe.Encoder[ErrorPayload].apply(ep), + mode.signal, + modalBus + ) ) - given JsonValueCodec[Option[Payload]] = JsonCodecMaker.make - def displayPayload(name: String, op: Option[Payload]) = div( bar(b(color := "lightgreen", name)), - displayJson(op, mode.signal, modalBus) + displayJson( + op.map(_.data).getOrElse(io.circe.Json.Null), + mode.signal, + modalBus + ) ) div( @@ -129,22 +131,11 @@ def jsonViewer( ) end jsonViewer -def displayJson[T: JsonValueCodec]( - rmsg: T, +def displayJson( + js: io.circe.Json, mode: Signal[JsonMode], modalBus: EventBus[ModalCommand] ) = - val js = io.circe.scalajs - .decodeJs[io.circe.Json]( - JSON.parse( - writeToString[T]( - rmsg, - WriterConfig.withIndentionStep(0) - ) - ) - ) - .fold(throw _, identity) - org.scalajs.dom.console.log(js) import io.circe.* diff --git a/modules/tracer/shared/src/main/scala/RawMessage.scala b/modules/tracer/shared/src/main/scala/RawMessage.scala index e4fb0d41c..c0f92ef56 100644 --- a/modules/tracer/shared/src/main/scala/RawMessage.scala +++ b/modules/tracer/shared/src/main/scala/RawMessage.scala @@ -55,6 +55,7 @@ end RawMessage object RawMessage: import com.github.plokhotnyuk.jsoniter_scala.macros.* + import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.* given JsonValueCodec[RawMessage] = JsonCodecMaker.make