From b6d8a5788a3e2af7163472420343ef9815d3fe32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 7 May 2025 22:21:59 +0200 Subject: [PATCH 01/16] bump jsonrpclib --- build.sbt | 2 +- modules/app/src/main/scala/LangoustineApp.scala | 3 ++- modules/tracer/backend/src/main/scala/command.trace.scala | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build.sbt b/build.sbt index 6508f815d..8c8e914d5 100644 --- a/build.sbt +++ b/build.sbt @@ -33,7 +33,7 @@ val V = new { val scribe = "3.13.1" val upickle = "2.0.0" val cats = "2.10.0" - val jsonrpclib = "0.0.7" + val jsonrpclib = "0.0.7+27-4fdf7547-SNAPSHOT" val fs2 = "3.10.0" val http4s = "0.23.26" val laminar = "0.14.5" diff --git a/modules/app/src/main/scala/LangoustineApp.scala b/modules/app/src/main/scala/LangoustineApp.scala index fc0ac26c3..67f50cc68 100644 --- a/modules/app/src/main/scala/LangoustineApp.scala +++ b/modules/app/src/main/scala/LangoustineApp.scala @@ -131,7 +131,8 @@ object LangoustineApp: fs2.Stream .eval(IO.deferred[Boolean]) .flatMap { latch => - FS2Channel[IO](bufferSize, None) + FS2Channel + .stream[IO](bufferSize, None) .flatMap { channel => (builder: @unchecked) match case l: LSPBuilder[IO] => diff --git a/modules/tracer/backend/src/main/scala/command.trace.scala b/modules/tracer/backend/src/main/scala/command.trace.scala index b78e85d36..6864ef3fe 100644 --- a/modules/tracer/backend/src/main/scala/command.trace.scala +++ b/modules/tracer/backend/src/main/scala/command.trace.scala @@ -65,7 +65,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 From d54eceae27b428efdd292dc1af5c85626668360e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 7 May 2025 22:24:52 +0200 Subject: [PATCH 02/16] bump scalajs --- project/plugins.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/project/plugins.sbt b/project/plugins.sbt index f5196057c..e57025cf3 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -4,7 +4,7 @@ addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.12.0") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.9.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.16.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.7") addSbtPlugin("com.github.reibitto" % "sbt-welcome" % "0.2.2") From 35144b6164ef1b460ea34f98a4fd6d5fb9aa9669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 7 May 2025 22:28:39 +0200 Subject: [PATCH 03/16] Add sonatype snapshots --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 8c8e914d5..e25db14af 100644 --- a/build.sbt +++ b/build.sbt @@ -24,7 +24,8 @@ inThisBuild( "keynmol@gmail.com", url("https://blog.indoorvivants.com") ) - ) + ), + resolvers ++= Resolver.sonatypeOssRepos("snapshots") ) ) From 20b63e992ebeb265877a57be5fadcacec5ba5813 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 7 May 2025 22:44:50 +0200 Subject: [PATCH 04/16] publish always, yolo --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9fefc88e..420875073 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,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 }} From c1af97d994c89b5c535e6c4c9dfff8b24b96bd39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 16 May 2025 01:30:41 +0200 Subject: [PATCH 05/16] bump to latest --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 48585ce21..d86d98243 100644 --- a/build.sbt +++ b/build.sbt @@ -31,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+27-4fdf7547-SNAPSHOT" + val jsonrpclib = "0.0.7+36-edbb9b01-SNAPSHOT" val fs2 = "3.10.0" val http4s = "0.23.26" val laminar = "0.14.5" From 67f3f638703ab78723985199bbcda5b74b14bf3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 16 May 2025 01:49:19 +0200 Subject: [PATCH 06/16] wip --- .../app/src/main/scala/LangoustineApp.scala | 17 +++---- modules/lsp/src/main/scala/JSONRPC.scala | 46 ++++++++++--------- .../tracer/backend/src/main/scala/state.scala | 1 - .../shared/src/main/scala/RawMessage.scala | 2 +- 4 files changed, 32 insertions(+), 34 deletions(-) diff --git a/modules/app/src/main/scala/LangoustineApp.scala b/modules/app/src/main/scala/LangoustineApp.scala index 4d027ad59..458c8bd8a 100644 --- a/modules/app/src/main/scala/LangoustineApp.scala +++ b/modules/app/src/main/scala/LangoustineApp.scala @@ -76,18 +76,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 diff --git a/modules/lsp/src/main/scala/JSONRPC.scala b/modules/lsp/src/main/scala/JSONRPC.scala index f0422dee9..c00db7ee0 100644 --- a/modules/lsp/src/main/scala/JSONRPC.scala +++ b/modules/lsp/src/main/scala/JSONRPC.scala @@ -23,31 +23,35 @@ 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 private[langoustine] 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) + // 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(readFromString[io.circe.Json](write(a))) + ??? def handlerToEndpoint[F[_]: Monadic, T <: LSPRequest](req: T)( f: req.In => F[req.Out] diff --git a/modules/tracer/backend/src/main/scala/state.scala b/modules/tracer/backend/src/main/scala/state.scala index 38d9a1819..24838b4e5 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 diff --git a/modules/tracer/shared/src/main/scala/RawMessage.scala b/modules/tracer/shared/src/main/scala/RawMessage.scala index e4fb0d41c..b567c6ef0 100644 --- a/modules/tracer/shared/src/main/scala/RawMessage.scala +++ b/modules/tracer/shared/src/main/scala/RawMessage.scala @@ -56,7 +56,7 @@ end RawMessage object RawMessage: import com.github.plokhotnyuk.jsoniter_scala.macros.* - given JsonValueCodec[RawMessage] = JsonCodecMaker.make + given JsonValueCodec[RawMessage] = ??? // JsonCodecMaker.make def create( method: Option[String] = None, From b3bfd7f87293800017fb1b7006286a326545aaa2 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Fri, 16 May 2025 19:51:55 +0200 Subject: [PATCH 07/16] Replace jsoniter with circe in tracer --- build.sbt | 10 +++++- .../src/main/scala/command.trace.scala | 2 +- .../tracer/backend/src/main/scala/state.scala | 11 +++++-- .../src/main/scala/component.jsonviewer.scala | 33 +++++++------------ 4 files changed, 30 insertions(+), 26 deletions(-) diff --git a/build.sbt b/build.sbt index d86d98243..e84001e24 100644 --- a/build.sbt +++ b/build.sbt @@ -262,6 +262,10 @@ lazy val tracer = projectMatrix 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" % "2.35.3" % "compile-internal" + ), scalacOptions ++= commonScalacOptions, Compile / doc / sources := Seq.empty, // embedding frontend in backend's resources @@ -323,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) diff --git a/modules/tracer/backend/src/main/scala/command.trace.scala b/modules/tracer/backend/src/main/scala/command.trace.scala index 8a8974fd4..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")}" ) ) diff --git a/modules/tracer/backend/src/main/scala/state.scala b/modules/tracer/backend/src/main/scala/state.scala index 24838b4e5..fa27aaec0 100644 --- a/modules/tracer/backend/src/main/scala/state.scala +++ b/modules/tracer/backend/src/main/scala/state.scala @@ -28,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], @@ -110,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.* From 0eebf159565eab801cd41f610928f30784f25ac0 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Fri, 16 May 2025 21:22:55 +0200 Subject: [PATCH 08/16] fix tracer tests --- build.sbt | 1 + modules/tests/src/test/scala/testkit.scala | 66 ++++++++++--------- .../scalajvm/tracer/TracerServerSpec.scala | 7 +- .../shared/src/main/scala/RawMessage.scala | 3 +- 4 files changed, 41 insertions(+), 36 deletions(-) diff --git a/build.sbt b/build.sbt index e84001e24..c313fbbbf 100644 --- a/build.sbt +++ b/build.sbt @@ -344,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/tests/src/test/scala/testkit.scala b/modules/tests/src/test/scala/testkit.scala index 0d1a73879..71e0c01dc 100644 --- a/modules/tests/src/test/scala/testkit.scala +++ b/modules/tests/src/test/scala/testkit.scala @@ -78,26 +78,27 @@ def request[F[_]: RefConstructor: MonadThrow, T <: requests.LSPRequest]( 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 - } + .collectFirst { case _ => F.pure(null.asInstanceOf[req.Out]-> communicate)} + // .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") @@ -118,18 +119,19 @@ def notification[F[ 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 - } + .collectFirst { case _ => F.pure(communicate)} + // .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") diff --git a/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala b/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala index 643d1b44f..98fac6ddc 100644 --- a/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala +++ b/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala @@ -10,6 +10,7 @@ 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 @@ -48,7 +49,7 @@ object TracerServerSpec extends ServerSpec: clientResp1 = RawMessage.create( id = id, - result = Some(Payload("{}".getBytes())) + result = Some(Payload(Json.obj())) ) _ <- serv.send(_.in, clientResp1) @@ -66,10 +67,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/shared/src/main/scala/RawMessage.scala b/modules/tracer/shared/src/main/scala/RawMessage.scala index b567c6ef0..c0f92ef56 100644 --- a/modules/tracer/shared/src/main/scala/RawMessage.scala +++ b/modules/tracer/shared/src/main/scala/RawMessage.scala @@ -55,8 +55,9 @@ end RawMessage object RawMessage: import com.github.plokhotnyuk.jsoniter_scala.macros.* + import com.github.plokhotnyuk.jsoniter_scala.circe.JsoniterScalaCodec.* - given JsonValueCodec[RawMessage] = ??? // JsonCodecMaker.make + given JsonValueCodec[RawMessage] = JsonCodecMaker.make def create( method: Option[String] = None, From 628028d16bee160d9d08bf7d14dccceaf752a2a2 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sat, 17 May 2025 00:16:53 +0200 Subject: [PATCH 09/16] Fix LSPTests --- .../app/src/main/scala/LangoustineApp.scala | 8 +- modules/lsp/src/main/scala/JSONRPC.scala | 49 +++-- modules/lsp/src/main/scala/LSPBuilder.scala | 10 +- modules/tests/src/test/scala/LSPTests.scala | 199 ++++++++++++------ modules/tests/src/test/scala/testkit.scala | 19 +- 5 files changed, 194 insertions(+), 91 deletions(-) diff --git a/modules/app/src/main/scala/LangoustineApp.scala b/modules/app/src/main/scala/LangoustineApp.scala index 458c8bd8a..518215aa9 100644 --- a/modules/app/src/main/scala/LangoustineApp.scala +++ b/modules/app/src/main/scala/LangoustineApp.scala @@ -131,9 +131,11 @@ object LangoustineApp: .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)) + ) + .as(channel) // ??? case other: LSPServerBuilder[IO] => FS2.Stream .resource(other(channel, latch.complete(true).void)) diff --git a/modules/lsp/src/main/scala/JSONRPC.scala b/modules/lsp/src/main/scala/JSONRPC.scala index c00db7ee0..0bffabe9a 100644 --- a/modules/lsp/src/main/scala/JSONRPC.scala +++ b/modules/lsp/src/main/scala/JSONRPC.scala @@ -26,32 +26,39 @@ 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 - // ) + new Codec[T]: - // case e => - // ProtocolError.InternalError("oopsie daisy: " + e.getMessage) - // } + 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 + ) - // override def encode(a: T): Payload = - // Payload(readFromString[io.circe.Json](write(a))) - ??? + 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/lsp/src/main/scala/LSPBuilder.scala b/modules/lsp/src/main/scala/LSPBuilder.scala index b1104251c..5441c9a31 100644 --- a/modules/lsp/src/main/scala/LSPBuilder.scala +++ b/modules/lsp/src/main/scala/LSPBuilder.scala @@ -68,12 +68,18 @@ trait LSPBuilder[F[_]]: end match end bind - def bind[T <: Channel[F]](channel: T, shutdown: F[Unit])(using + def bind(channel: Channel[F], shutdown: F[Unit])(using Monadic[F] - ): F[T] = + ): F[Channel[F]] = bind(channel, Communicate.channel(channel, shutdown)) end bind + def bind3(channel: Channel[F], shutdown: F[Unit])(using + Monadic[F] + ): F[Channel[F]] = + bind(channel, Communicate.channel(channel, shutdown)) + end bind3 + end LSPBuilder object LSPBuilder: diff --git a/modules/tests/src/test/scala/LSPTests.scala b/modules/tests/src/test/scala/LSPTests.scala index faa09e0db..4dacba6ef 100644 --- a/modules/tests/src/test/scala/LSPTests.scala +++ b/modules/tests/src/test/scala/LSPTests.scala @@ -6,14 +6,49 @@ import scala.util.* import langoustine.lsp.all.* +import fs2.Stream +import jsonrpclib.* +import jsonrpclib.fs2.FS2Channel +import cats.effect.IO +import scala.concurrent.duration.* +import langoustine.lsp.jsonrpcIntegration.given + +import weaver.* +import _root_.fs2.concurrent.SignallingRef + def basicServer[F[_]: Monadic] = LSPBuilder.create[F] -import jsonrpclib.* +object LSPTests extends SimpleIOSuite: + + 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 -object LSPTests extends weaver.FunSuite: - test("initialize") { + def testRes(name: TestName)(run: Stream[IO, Expectations]): Unit = + test(name)(run.compile.lastOrError.timeout(10.second)) + testRes("initialize") { import requests.* val capabilities = @@ -23,56 +58,87 @@ 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 + c <- setupChannels( + mkServer = cc => + basicServer[IO] + .handleRequest(initialize) { in => + IO(InitializeResult(capabilities)) + } + .build(Communicate.channel(cc, IO.unit)) ) - ).get - expect.same(response.capabilities, capabilities) + remoteCall = c.simpleStub[initialize.In, initialize.Out]( + initialize.requestMethod + ) + response <- Stream.eval( + remoteCall( + InitializeParams( + processId = Opt(25), + rootUri = Opt(DocumentUri("/howdy")), + capabilities = ClientCapabilities() + ) + ) + ) + yield expect.same(response.capabilities, capabilities) + end for } - test("didOpen") { + testRes("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) + ) + c <- setupChannels( + mkServer = cc => + 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(cc, IO.unit)), + mkClient = _ => + List( + Endpoint[IO](window.showMessage.notificationMethod) + .notification[window.showMessage.In](in => + ref.update { mp => + mp.updatedWith(window.showMessage) { prev => + prev.map(_ :+ in).orElse(Some(List(in))) + } + } + ) + ) + ) + + remoteCall = c.notificationStub[textDocument.didOpen.In]( + textDocument.didOpen.notificationMethod + ) + _ <- Stream.eval( + remoteCall( + 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 <- ref.discrete + .dropWhile(_.isEmpty) + .map(_.get(window.showMessage)) + .collectFirst { case Some(v) => v } + yield expect.same( + response, List( ShowMessageParams( MessageType.Info, @@ -80,11 +146,12 @@ object LSPTests extends weaver.FunSuite: ) ) ) - + end for } - test("textDocument/documentSymbol") { + testRes("textDocument/documentSymbol") { import requests.* + import textDocument.documentSymbol.given val symbols: Opt[Vector[DocumentSymbol]] = Opt( @@ -108,25 +175,31 @@ 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 - ) + remoteCall = channel.simpleStub[ + textDocument.documentSymbol.In, + textDocument.documentSymbol.Out + ]( + textDocument.documentSymbol.requestMethod + ) + response <- Stream.eval( + remoteCall( + 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 71e0c01dc..784e2fef7 100644 --- a/modules/tests/src/test/scala/testkit.scala +++ b/modules/tests/src/test/scala/testkit.scala @@ -11,6 +11,8 @@ import java.util.concurrent.atomic.AtomicReference import jsonrpclib.InputMessage.RequestMessage import jsonrpclib.InputMessage.NotificationMessage import com.github.plokhotnyuk.jsoniter_scala.core.writeToArray +import cats.effect.kernel.Ref +import cats.effect.IO given [F[_]](using MonadThrow[F]): Monadic[F] with def doFlatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = @@ -78,7 +80,9 @@ def request[F[_]: RefConstructor: MonadThrow, T <: requests.LSPRequest]( builder .build(communicate) .find(_.method == req.requestMethod) - .collectFirst { case _ => F.pure(null.asInstanceOf[req.Out]-> communicate)} + .collectFirst { case _ => + F.pure(null.asInstanceOf[req.Out] -> communicate) + } // .collectFirst { // case Endpoint.RequestResponseEndpoint(_, run, inc, erc, outc) => // val encoded = Payload(upickle.default.write(in).getBytes()) @@ -119,7 +123,7 @@ def notification[F[ builder .build(communicate) .find(_.method == req.notificationMethod) - .collectFirst { case _ => F.pure(communicate)} + .collectFirst { case _ => F.pure(communicate) } // .collectFirst { case Endpoint.NotificationEndpoint(_, run, inc) => // val encoded = Payload(upickle.default.write(in).getBytes()) // for @@ -155,3 +159,14 @@ given RefConstructor[Try] with } end given + +given RefConstructor[IO] with + def apply[A](a: A): IO[RefLike[IO, A]] = + Ref + .of[IO, A](a) + .map(r => + new RefLike[IO, A]: + def get: IO[A] = r.get + def update(f: A => A): IO[Unit] = r.update(f) + ) +end given From d874825a2a43a59c9367f699aeb5da7ec0c36a24 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sat, 17 May 2025 00:20:12 +0200 Subject: [PATCH 10/16] Revert redundant changes --- modules/app/src/main/scala/LangoustineApp.scala | 1 - modules/lsp/src/main/scala/LSPBuilder.scala | 10 ++-------- modules/tests/src/test/scala/testkit.scala | 16 ---------------- 3 files changed, 2 insertions(+), 25 deletions(-) diff --git a/modules/app/src/main/scala/LangoustineApp.scala b/modules/app/src/main/scala/LangoustineApp.scala index 518215aa9..43c03a073 100644 --- a/modules/app/src/main/scala/LangoustineApp.scala +++ b/modules/app/src/main/scala/LangoustineApp.scala @@ -135,7 +135,6 @@ object LangoustineApp: .resource( Resource.eval(l.bind(channel, latch.complete(true).void)) ) - .as(channel) // ??? case other: LSPServerBuilder[IO] => FS2.Stream .resource(other(channel, latch.complete(true).void)) diff --git a/modules/lsp/src/main/scala/LSPBuilder.scala b/modules/lsp/src/main/scala/LSPBuilder.scala index 5441c9a31..b1104251c 100644 --- a/modules/lsp/src/main/scala/LSPBuilder.scala +++ b/modules/lsp/src/main/scala/LSPBuilder.scala @@ -68,18 +68,12 @@ trait LSPBuilder[F[_]]: end match end bind - def bind(channel: Channel[F], shutdown: F[Unit])(using + def bind[T <: Channel[F]](channel: T, shutdown: F[Unit])(using Monadic[F] - ): F[Channel[F]] = + ): F[T] = bind(channel, Communicate.channel(channel, shutdown)) end bind - def bind3(channel: Channel[F], shutdown: F[Unit])(using - Monadic[F] - ): F[Channel[F]] = - bind(channel, Communicate.channel(channel, shutdown)) - end bind3 - end LSPBuilder object LSPBuilder: diff --git a/modules/tests/src/test/scala/testkit.scala b/modules/tests/src/test/scala/testkit.scala index 784e2fef7..3d5c73586 100644 --- a/modules/tests/src/test/scala/testkit.scala +++ b/modules/tests/src/test/scala/testkit.scala @@ -8,11 +8,6 @@ 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 -import cats.effect.kernel.Ref -import cats.effect.IO given [F[_]](using MonadThrow[F]): Monadic[F] with def doFlatMap[A, B](fa: F[A])(f: A => F[B]): F[B] = @@ -159,14 +154,3 @@ given RefConstructor[Try] with } end given - -given RefConstructor[IO] with - def apply[A](a: A): IO[RefLike[IO, A]] = - Ref - .of[IO, A](a) - .map(r => - new RefLike[IO, A]: - def get: IO[A] = r.get - def update(f: A => A): IO[Unit] = r.update(f) - ) -end given From 7f63d24746605f2ae294ad42be15c8c8ac79eebc Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sat, 17 May 2025 12:36:05 +0200 Subject: [PATCH 11/16] Refactor testkit --- modules/tests/src/test/scala/LSPTests.scala | 122 ++++-------- modules/tests/src/test/scala/testkit.scala | 196 ++++++-------------- 2 files changed, 92 insertions(+), 226 deletions(-) diff --git a/modules/tests/src/test/scala/LSPTests.scala b/modules/tests/src/test/scala/LSPTests.scala index 4dacba6ef..4ebfbbe0e 100644 --- a/modules/tests/src/test/scala/LSPTests.scala +++ b/modules/tests/src/test/scala/LSPTests.scala @@ -2,16 +2,15 @@ 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 jsonrpclib.fs2.FS2Channel import cats.effect.IO import scala.concurrent.duration.* -import langoustine.lsp.jsonrpcIntegration.given import weaver.* import _root_.fs2.concurrent.SignallingRef @@ -21,34 +20,10 @@ def basicServer[F[_]: Monadic] = object LSPTests extends SimpleIOSuite: - 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 testRes(name: TestName)(run: Stream[IO, Expectations]): Unit = + def testStream(name: TestName)(run: Stream[IO, Expectations]): Unit = test(name)(run.compile.lastOrError.timeout(10.second)) - testRes("initialize") { + testStream("initialize") { import requests.* val capabilities = @@ -59,25 +34,21 @@ object LSPTests extends SimpleIOSuite: ) for - c <- setupChannels( - mkServer = cc => + channel <- setupChannels( + mkServer = channel => basicServer[IO] .handleRequest(initialize) { in => IO(InitializeResult(capabilities)) } - .build(Communicate.channel(cc, IO.unit)) - ) - - remoteCall = c.simpleStub[initialize.In, initialize.Out]( - initialize.requestMethod + .build(Communicate.channel(channel, IO.unit)) ) - response <- Stream.eval( - remoteCall( - InitializeParams( - processId = Opt(25), - rootUri = Opt(DocumentUri("/howdy")), - capabilities = ClientCapabilities() - ) + response <- request( + channel, + initialize, + InitializeParams( + processId = Opt(25), + rootUri = Opt(DocumentUri("/howdy")), + capabilities = ClientCapabilities() ) ) yield expect.same(response.capabilities, capabilities) @@ -85,15 +56,15 @@ object LSPTests extends SimpleIOSuite: } - testRes("didOpen") { + testStream("didOpen") { import requests.* for ref <- Stream.eval( SignallingRef[IO, Map[LSPNotification, List[Any]]](Map.empty) ) - c <- setupChannels( - mkServer = cc => + channel <- setupChannels( + mkServer = channel => basicServer[IO] .handleNotification(textDocument.didOpen) { in => in.toClient.notification( @@ -104,39 +75,23 @@ object LSPTests extends SimpleIOSuite: ) ) } - .build(Communicate.channel(cc, IO.unit)), - mkClient = _ => - List( - Endpoint[IO](window.showMessage.notificationMethod) - .notification[window.showMessage.In](in => - ref.update { mp => - mp.updatedWith(window.showMessage) { prev => - prev.map(_ :+ in).orElse(Some(List(in))) - } - } - ) - ) + .build(Communicate.channel(channel, IO.unit)), + mkClient = + _ => List(collectNotificationEndpoint(ref)(window.showMessage)) ) - - remoteCall = c.notificationStub[textDocument.didOpen.In]( - textDocument.didOpen.notificationMethod - ) - _ <- Stream.eval( - remoteCall( - DidOpenTextDocumentParams( - TextDocumentItem( - uri = DocumentUri("/home/bla.txt"), - languageId = "text", - version = 0, - text = "Hello!" - ) + _ <- notification( + channel, + textDocument.didOpen, + DidOpenTextDocumentParams( + TextDocumentItem( + uri = DocumentUri("/home/bla.txt"), + languageId = "text", + version = 0, + text = "Hello!" ) ) ) - response <- ref.discrete - .dropWhile(_.isEmpty) - .map(_.get(window.showMessage)) - .collectFirst { case Some(v) => v } + response <- getCaputured(ref.discrete)(window.showMessage) yield expect.same( response, List( @@ -149,9 +104,8 @@ object LSPTests extends SimpleIOSuite: end for } - testRes("textDocument/documentSymbol") { + testStream("textDocument/documentSymbol") { import requests.* - import textDocument.documentSymbol.given val symbols: Opt[Vector[DocumentSymbol]] = Opt( @@ -185,17 +139,11 @@ object LSPTests extends SimpleIOSuite: .build(Communicate.channel(channel, IO.unit)) ) - remoteCall = channel.simpleStub[ - textDocument.documentSymbol.In, - textDocument.documentSymbol.Out - ]( - textDocument.documentSymbol.requestMethod - ) - response <- Stream.eval( - remoteCall( - DocumentSymbolParams( - TextDocumentIdentifier(DocumentUri("/home/bla.txt")) - ) + response <- request( + channel, + textDocument.documentSymbol, + DocumentSymbolParams( + TextDocumentIdentifier(DocumentUri("/home/bla.txt")) ) ) yield expect.same(response, symbols) diff --git a/modules/tests/src/test/scala/testkit.scala b/modules/tests/src/test/scala/testkit.scala index 3d5c73586..2fdda3559 100644 --- a/modules/tests/src/test/scala/testkit.scala +++ b/modules/tests/src/test/scala/testkit.scala @@ -1,156 +1,74 @@ 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 - -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 _ => - F.pure(null.asInstanceOf[req.Out] -> communicate) - } - // .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 _ => F.pure(communicate) } - // .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]] } From ec9c9f2d79a762f3598e9417c744fd7185159d3c Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sat, 17 May 2025 12:44:42 +0200 Subject: [PATCH 12/16] Fix EndToEndTests --- .../src/test/scala/EndToEndTests.scala | 27 ++++++------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/modules/e2e-tests/src/test/scala/EndToEndTests.scala b/modules/e2e-tests/src/test/scala/EndToEndTests.scala index a7ec3a434..11627b511 100644 --- a/modules/e2e-tests/src/test/scala/EndToEndTests.scala +++ b/modules/e2e-tests/src/test/scala/EndToEndTests.scala @@ -1,26 +1,15 @@ 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]) @@ -150,7 +139,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 @@ -163,7 +152,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 From 4174ad9f34fff1a3eb9de9dee0a69e061cac42a1 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Sat, 17 May 2025 12:50:55 +0200 Subject: [PATCH 13/16] Update to latest snapshot --- build.sbt | 2 +- modules/app/src/main/scala/LangoustineApp.scala | 3 --- modules/tests/src/test/scala/CodecTest.scala | 5 ----- modules/tests/src/test/scalajvm/tracer/Feed.scala | 7 ------- modules/tests/src/test/scalajvm/tracer/Front.scala | 7 +------ modules/tests/src/test/scalajvm/tracer/ServerSpec.scala | 2 -- .../tests/src/test/scalajvm/tracer/TracerServerSpec.scala | 5 +---- 7 files changed, 3 insertions(+), 28 deletions(-) diff --git a/build.sbt b/build.sbt index c313fbbbf..b376b73cc 100644 --- a/build.sbt +++ b/build.sbt @@ -31,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+36-edbb9b01-SNAPSHOT" + val jsonrpclib = "0.0.8+38-c159559c-SNAPSHOT" val fs2 = "3.10.0" val http4s = "0.23.26" val laminar = "0.14.5" diff --git a/modules/app/src/main/scala/LangoustineApp.scala b/modules/app/src/main/scala/LangoustineApp.scala index 43c03a073..aae4ecb23 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 diff --git a/modules/tests/src/test/scala/CodecTest.scala b/modules/tests/src/test/scala/CodecTest.scala index b5e881329..2b7bed6e1 100644 --- a/modules/tests/src/test/scala/CodecTest.scala +++ b/modules/tests/src/test/scala/CodecTest.scala @@ -2,14 +2,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/scalajvm/tracer/Feed.scala b/modules/tests/src/test/scalajvm/tracer/Feed.scala index 345cb46c0..e8feff024 100644 --- a/modules/tests/src/test/scalajvm/tracer/Feed.scala +++ b/modules/tests/src/test/scalajvm/tracer/Feed.scala @@ -1,20 +1,13 @@ 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 29d88b955..d59d65918 100644 --- a/modules/tests/src/test/scalajvm/tracer/Front.scala +++ b/modules/tests/src/test/scalajvm/tracer/Front.scala @@ -1,17 +1,11 @@ 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 @@ -61,4 +55,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 86988e8fe..406ccb929 100644 --- a/modules/tests/src/test/scalajvm/tracer/ServerSpec.scala +++ b/modules/tests/src/test/scalajvm/tracer/ServerSpec.scala @@ -1,8 +1,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 98fac6ddc..295e2aa1f 100644 --- a/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala +++ b/modules/tests/src/test/scalajvm/tracer/TracerServerSpec.scala @@ -1,12 +1,9 @@ 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.* From 9780297b7980d97d26d0440bca7aa821a0e41529 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Thu, 29 May 2025 22:41:15 +0200 Subject: [PATCH 14/16] Update jsonrpclib snapshot --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index b376b73cc..d26e193a0 100644 --- a/build.sbt +++ b/build.sbt @@ -31,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.8+38-c159559c-SNAPSHOT" + val jsonrpclib = "0.0.8+44-ea0af08a-SNAPSHOT" val fs2 = "3.10.0" val http4s = "0.23.26" val laminar = "0.14.5" From c1b8fef66952bf230115e2dc5610f89f91b294f3 Mon Sep 17 00:00:00 2001 From: Kasper Kondzielski <5662622+ghostbuster91@users.noreply.github.com> Date: Fri, 11 Jul 2025 11:27:02 +0200 Subject: [PATCH 15/16] Update build.sbt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jakub Kozłowski --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index d26e193a0..f2a49f354 100644 --- a/build.sbt +++ b/build.sbt @@ -31,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.8+44-ea0af08a-SNAPSHOT" + val jsonrpclib = "0.1.0" val fs2 = "3.10.0" val http4s = "0.23.26" val laminar = "0.14.5" From 71da7517298c2c1ace031d806105dcdb2ab0d462 Mon Sep 17 00:00:00 2001 From: ghostbuster91 Date: Fri, 11 Jul 2025 17:51:43 +0200 Subject: [PATCH 16/16] Define circe codec for LSPCancelRequest --- build.sbt | 6 +++--- modules/app/src/main/scala/LSPCancelRequest.scala | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/build.sbt b/build.sbt index f2a49f354..be2fda93d 100644 --- a/build.sbt +++ b/build.sbt @@ -148,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) @@ -256,7 +256,7 @@ 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, @@ -264,7 +264,7 @@ lazy val tracer = projectMatrix 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" % "2.35.3" % "compile-internal" + "com.github.plokhotnyuk.jsoniter-scala" %% "jsoniter-scala-macros" % V.jsoniter % "compile-internal" ), scalacOptions ++= commonScalacOptions, Compile / doc / sources := Seq.empty, 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](