From 95d55dbf29a511659b609e9b6bc816b84e773ac0 Mon Sep 17 00:00:00 2001 From: Anton Sviridov Date: Sat, 3 Sep 2022 12:10:25 +0100 Subject: [PATCH] Render And types in requests (#49) * Render And types in requests * Drop down to Scala 3.1 for now * Commit unspeakable sins to have a pretty website --- .github/workflows/ci.yml | 6 +- build.sbt | 108 +++++++------ modules/generate/src/main/scala/render.scala | 148 +++++++++++++++--- .../lsp/src/main/scala/generated/codecs.scala | 10 +- .../src/main/scala/generated/requests.scala | 14 +- modules/lsp/src/main/scala/newtype.scala | 4 +- modules/lsp/src/test/scala/CodecTest.scala | 17 +- .../test/scala/{Test.scala => LSPSuite.scala} | 2 +- modules/meta/src/main/scala/Manager.scala | 5 +- modules/meta/src/main/scala/newtype.scala | 5 +- 10 files changed, 231 insertions(+), 88 deletions(-) rename modules/lsp/src/test/scala/{Test.scala => LSPSuite.scala} (97%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3197ad0a..d4ebef28a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: cache: 'sbt' - name: Test - run: sbt --client 'ci; publishLocal; lsp/doc' + run: sbt --client 'ci; publishLocal' - name: Publish ${{ github.ref }} if: startsWith(github.ref, 'refs/tags/v') @@ -44,7 +44,9 @@ jobs: - name: Build API doc if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main') - run: sbt --client "lsp/doc" + run: sbt "lsp/doc" + env: + USE_SCALA_NIGHTLY: true - name: Publish gh-pages if: startsWith(github.ref, 'refs/tags/v') || (github.ref == 'refs/heads/main') diff --git a/build.sbt b/build.sbt index 508cb356b..2b8e3e7a4 100644 --- a/build.sbt +++ b/build.sbt @@ -30,16 +30,34 @@ inThisBuild( ) val V = new { - val scala = "3.1.3" - val scribe = "3.10.3" - val upickle = "2.0.0" - val cats = "2.8.0" - val verify = "1.0.0" - val jsonrpclib = "0.0.3" - val fs2 = "3.2.12" - val http4s = "0.23.15" - val laminar = "0.14.2" - val decline = "2.3.0" + val scala = "3.1.3" + val scalaNightly = "3.2.1-RC1-bin-20220902-3f0c6d3-NIGHTLY" + val scribe = "3.10.3" + val upickle = "2.0.0" + val cats = "2.8.0" + val munit = "1.0.0-M6" + val jsonrpclib = "0.0.3" + val fs2 = "3.2.12" + val http4s = "0.23.15" + val laminar = "0.14.2" + val decline = "2.3.0" + val jsoniter = "2.17.0" + val weaver = "0.7.15" + val http4sJdkClient = "0.7.0" + + /** TODO: remove all the nightly hacks once the deliciously decadent scaladoc + * facelift is released (3.2.1?) + */ + + private val dynScalaVersion = + if (sys.env.contains("USE_SCALA_NIGHTLY")) scalaNightly + else scala + + val jvmScalaVersions = List(dynScalaVersion) + val scalaVersions = List(scala) + + val default = + Seq(VirtualAxis.scalaABIVersion(dynScalaVersion), VirtualAxis.jvm) } lazy val noPublishing = Seq( @@ -47,10 +65,6 @@ lazy val noPublishing = Seq( publishLocal / skip := true ) -val scalaVersions = List(V.scala) - -val default = Seq(VirtualAxis.scalaABIVersion(V.scala), VirtualAxis.jvm) - lazy val root = project .in(file(".")) .aggregate(meta.projectRefs*) @@ -63,10 +77,10 @@ lazy val root = project lazy val meta = projectMatrix .in(file("modules/meta")) .settings(name := "langoustine-meta") - .defaultAxes(default*) - .jvmPlatform(scalaVersions) - .jsPlatform(scalaVersions) - .nativePlatform(scalaVersions) + .defaultAxes(V.default*) + .jvmPlatform(V.jvmScalaVersions) + .jsPlatform(V.scalaVersions) + .nativePlatform(V.scalaVersions) .settings( libraryDependencies += "com.outr" %%% "scribe" % V.scribe, libraryDependencies += "com.lihaoyi" %%% "upickle" % V.upickle, @@ -75,38 +89,37 @@ lazy val meta = projectMatrix lazy val lsp = projectMatrix .in(file("modules/lsp")) - .defaultAxes(default*) + .defaultAxes(V.default*) .settings( name := "langoustine-lsp", scalacOptions ++= Seq("-Xmax-inlines", "64"), - libraryDependencies += "com.eed3si9n.verify" %%% "verify" % V.verify % Test, - testFrameworks += new TestFramework("verify.runner.Framework"), - Test / fork := virtualAxes.value.contains(VirtualAxis.jvm), - libraryDependencies += "com.outr" %%% "scribe" % V.scribe, - libraryDependencies += "com.lihaoyi" %%% "upickle" % V.upickle, - libraryDependencies += "org.typelevel" %%% "cats-core" % V.cats, - libraryDependencies += "tech.neander" %%% "jsonrpclib-core" % V.jsonrpclib + libraryDependencies += "org.scalameta" %%% "munit" % V.munit % Test, + libraryDependencies += "com.outr" %%% "scribe" % V.scribe, + libraryDependencies += "com.lihaoyi" %%% "upickle" % V.upickle, + libraryDependencies += "org.typelevel" %%% "cats-core" % V.cats, + libraryDependencies += "tech.neander" %%% "jsonrpclib-core" % V.jsonrpclib, + Test / fork := virtualAxes.value.contains(VirtualAxis.jvm) ) - .jvmPlatform(scalaVersions) - .jsPlatform(scalaVersions) - .nativePlatform(scalaVersions) + .jvmPlatform(V.jvmScalaVersions) + .jsPlatform(V.scalaVersions) + .nativePlatform(V.scalaVersions) .settings(docsSettings) lazy val generate = projectMatrix .in(file("modules/generate")) .dependsOn(meta) - .defaultAxes(default*) + .defaultAxes(V.default*) .settings( name := "generate" ) - .jvmPlatform(scalaVersions) + .jvmPlatform(V.jvmScalaVersions) .settings(noPublishing) lazy val tracer = projectMatrix .in(file("modules/tracer/backend")) .dependsOn(lsp, tracerShared) .enablePlugins(JavaAppPackaging) - .defaultAxes(default*) + .defaultAxes(V.default*) .settings( name := "langoustine-tracer", libraryDependencies += "tech.neander" %%% "jsonrpclib-fs2" % V.jsonrpclib, @@ -147,7 +160,7 @@ lazy val tracer = projectMatrix } } ) - .jvmPlatform(scalaVersions) + .jvmPlatform(V.jvmScalaVersions) import org.scalajs.linker.interface.Report lazy val frontendJS = tracerFrontend.js(V.scala) @@ -166,46 +179,39 @@ ThisBuild / frontendOutput := { lazy val tracerFrontend = projectMatrix .in(file("modules/tracer/frontend")) .dependsOn(tracerShared) - .defaultAxes(default*) + .defaultAxes(V.default*) .settings( name := "langoustine-tracer-frontend", libraryDependencies += "com.raquo" %%% "laminar" % V.laminar, scalaJSUseMainModuleInitializer := true ) - .jsPlatform(scalaVersions) + .jsPlatform(V.scalaVersions) lazy val tracerShared = projectMatrix .in(file("modules/tracer/shared")) - .defaultAxes(default*) + .defaultAxes(V.default*) .settings( name := "langoustine-tracer-shared", libraryDependencies ++= Seq( - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % "2.17.0", - "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % "2.17.0" % "compile-internal", + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-core" % V.jsoniter, + "com.github.plokhotnyuk.jsoniter-scala" %%% "jsoniter-scala-macros" % V.jsoniter % "compile-internal", "tech.neander" %%% "jsonrpclib-core" % V.jsonrpclib ) ) - .jsPlatform(scalaVersions) - .jvmPlatform(scalaVersions) + .jsPlatform(V.scalaVersions) + .jvmPlatform(V.jvmScalaVersions) lazy val tracerTests = projectMatrix .in(file("modules/tracer/tests")) - .defaultAxes(default*) + .defaultAxes(V.default*) .dependsOn(tracer) .settings( libraryDependencies += "org.http4s" %%% "http4s-ember-client" % V.http4s % Test, - libraryDependencies += "com.disneystreaming" %% "weaver-cats" % "0.7.15" % Test, - libraryDependencies += "org.http4s" %% "http4s-jdk-http-client" % "0.7.0" % Test, + libraryDependencies += "com.disneystreaming" %% "weaver-cats" % V.weaver % Test, + libraryDependencies += "org.http4s" %% "http4s-jdk-http-client" % V.http4sJdkClient % Test, testFrameworks += new TestFramework("weaver.framework.CatsEffect") ) - .jvmPlatform(scalaVersions) - .settings(noPublishing) - -lazy val docs = projectMatrix - .in(file("docs")) - .dependsOn(lsp) - .jvmPlatform(scalaVersions) - .defaultAxes(default*) + .jvmPlatform(V.jvmScalaVersions) .settings(noPublishing) val scalafixRules = Seq( diff --git a/modules/generate/src/main/scala/render.scala b/modules/generate/src/main/scala/render.scala index 740291e69..e52de4358 100644 --- a/modules/generate/src/main/scala/render.scala +++ b/modules/generate/src/main/scala/render.scala @@ -1,6 +1,7 @@ package langoustine.generate import langoustine.meta.* +import langoustine.generate.StructureRenderConfig.PrivateCodecs class Render(manager: Manager, packageName: String = "langoustine.lsp"): private val INDENT = " " @@ -99,14 +100,76 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): val codecTraitName = s"requests_${req.method.value.replace('/', '_')}" + import Type.* + + def rewriteAndType(at: AndType, structName: StructureName) = + val resolved = at.items.collect { case r: ReferenceType => + manager.get(r.name.value).collect { case s: Structure => + s + } + + }.flatten + + if resolved.length == at.items.length then + Some(Structure(name = structName, mixins = at.items)) + else + scribe.error( + s"Found an AndType I cannot render, in request $req, resolved consistutents: $resolved" + ) + None + end rewriteAndType + + def rewriteAndParmas(p: ParamsType, structName: StructureName) = + p match + case ParamsType.Single(at: AndType) => + rewriteAndType(at, structName) + case _ => None + line( s"object ${req.name} extends LSPRequest(\"${req.method.value}\") with codecs.$codecTraitName:" ) nest { - line(s"type In = ${renderParams(req.params)}") - line(s"type Out = ${renderType(req.result)}") + val inStructName = + StructureName(req.segs.map(_.capitalize).mkString + "Input") + + val outStructName = + StructureName(req.segs.map(_.capitalize).mkString + "Output") + + val inStruct = rewriteAndParmas(req.params, inStructName) + val outStruct = Option(req.result).collect { case at: AndType => + rewriteAndType(at, outStructName) + }.flatten + + inStruct match + case Some(structure) => + line(s"type In = $inStructName") + case _ => + line(s"type In = ${renderParams(req.params)}") + + outStruct match + case Some(structure) => + line(s"type Out = $outStructName") + case _ => + line(s"type Out = ${renderType(req.result)}") + line("") + summon[Context].inModified( + _.copy(definitionScope = "requests" :: req.segs) + ) { + inStruct.foreach { + given StructureRenderConfig = StructureRenderConfig.default + .copy(privateCodecs = PrivateCodecs.Yes) + structure(_, out, codecsOut) + } + + outStruct.foreach { + given StructureRenderConfig = StructureRenderConfig.default + .copy(privateCodecs = PrivateCodecs.Yes) + structure(_, out, codecsOut) + } + } + codecsOut.topLevel { val path = req.segs .collect { @@ -123,12 +186,17 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): codecsLine(s"import $path.{In, Out}") - val reader = req.params match - case ParamsType.None => - WriterDefinition.Expression("unitReader") - case ParamsType.Single(t) => upickleReader1(t, "In") - case ParamsType.Many(t) => - WriterDefinition.Expression("Pickle.macroR") + val reader = + inStruct match + case None => + req.params match + case ParamsType.None => + WriterDefinition.Expression("unitReader") + case ParamsType.Single(t) => upickleReader1(t, "In") + case ParamsType.Many(t) => + WriterDefinition.Expression("Pickle.macroR") + case Some(s) => + WriterDefinition.Expression(s"$path.${s.name.value}.reader") codecsLine(s"given inputReader: Reader[In] = ") nest { @@ -139,18 +207,26 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): codecsLine(s"given inputWriter: Writer[In] = ") nest { - req.params match - case ParamsType.None => codecsLine("unitWriter") - case ParamsType.Single(t) => - upickleWriter(t, Some("In")).write(codecsOut) - case ParamsType.Many(t) => codecsLine("Pickle.macroR") + inStruct match + case None => + req.params match + case ParamsType.None => codecsLine("unitWriter") + case ParamsType.Single(t) => + upickleWriter(t, Some("In")).write(codecsOut) + case ParamsType.Many(t) => codecsLine("Pickle.macroR") + case Some(s) => + codecsLine(s"$path.${s.name.value}.writer") } codecsLine("") codecsLine(s"given outputWriter: Writer[Out] =") nest { - upickleWriter(req.result, Some("Out")).write(codecsOut) + outStruct match + case None => + upickleWriter(req.result, Some("Out")).write(codecsOut) + case Some(s) => + codecsLine(s"$path.${s.name.value}.writer") } codecsLine("") @@ -159,7 +235,11 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): s"given outputReader: Reader[Out] =" ) nest { - upickleReader1(req.result, "Out").write(codecsOut) + outStruct match + case None => + upickleReader1(req.result, "Out").write(codecsOut) + case Some(s) => + codecsLine(s"$path.${s.name.value}.reader") } } @@ -201,12 +281,13 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): nest { codecsLine(s"import $path.In") - val reader = req.params match - case ParamsType.None => - WriterDefinition.Expression("unitReader") - case ParamsType.Single(t) => upickleReader1(t, "In") - case ParamsType.Many(t) => - WriterDefinition.Expression("Pickle.macroR") + val reader = + req.params match + case ParamsType.None => + WriterDefinition.Expression("unitReader") + case ParamsType.Single(t) => upickleReader1(t, "In") + case ParamsType.Many(t) => + WriterDefinition.Expression("Pickle.macroR") codecsLine(s"given inputReader: Reader[In] = ") nest { @@ -416,8 +497,11 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): builder: LineBuilder, codecsOut: LineBuilder )(using - Config - )(using ctx: Context): Unit = + renderConfig: Config, + structConfig: StructureRenderConfig = + StructureRenderConfig(privateCodecs = PrivateCodecs.No), + ctx: Context + ): Unit = val props = Vector.newBuilder[String] val inlineStructures = Map.newBuilder[Type.ReferenceType, (String, Type.StructureLiteralType)] @@ -536,6 +620,9 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): inline def codecsLine: Config ?=> Appender = to(codecsOut) + val codecPublicity = + if structConfig.privateCodecs.yes then "private[lsp] " else "" + codecsOut.topLevel { codecsLine("") codecsLine(s"private[lsp] trait $fqfUnderscore:") @@ -560,10 +647,10 @@ class Render(manager: Manager, packageName: String = "langoustine.lsp"): } } codecsLine( - s"given reader: Reader[$fqf] = Pickle.macroR" + s"${codecPublicity}given reader: Reader[$fqf] = Pickle.macroR" ) codecsLine( - s"given writer: Writer[$fqf] = upickle.default.macroW" + s"${codecPublicity}given writer: Writer[$fqf] = upickle.default.macroW" ) } codecsLine("") @@ -1000,7 +1087,9 @@ object Types: case class Context( resolve: Type.ReferenceType => TypeName, definitionScope: List[String] - ) + ): + def inModified[A](f: Context => Context)(g: Context ?=> A) = + g(using f(this)) object Context: def global(manager: Manager, scope: List[String]) = @@ -1130,3 +1219,10 @@ object Constants: | */ |""".stripMargin.trim end Constants + +case class StructureRenderConfig(privateCodecs: PrivateCodecs) +object StructureRenderConfig: + inline def default = StructureRenderConfig(privateCodecs = PrivateCodecs.No) + + opaque type PrivateCodecs = Boolean + object PrivateCodecs extends YesNo[PrivateCodecs] diff --git a/modules/lsp/src/main/scala/generated/codecs.scala b/modules/lsp/src/main/scala/generated/codecs.scala index babcee6e0..5f381cb6e 100644 --- a/modules/lsp/src/main/scala/generated/codecs.scala +++ b/modules/lsp/src/main/scala/generated/codecs.scala @@ -1056,13 +1056,19 @@ private[lsp] trait requests_workspace_codeLens_refresh: given outputReader: Reader[Out] = nullReadWriter +private[lsp] trait requests_workspace_configuration_WorkspaceConfigurationInput: + import requests.workspace.configuration.* + private[lsp] given reader: Reader[requests.workspace.configuration.WorkspaceConfigurationInput] = Pickle.macroR + private[lsp] given writer: Writer[requests.workspace.configuration.WorkspaceConfigurationInput] = upickle.default.macroW + + private[lsp] trait requests_workspace_configuration: import workspace.configuration.{In, Out} given inputReader: Reader[In] = - ??? /* TODO: AndType(Vector(ReferenceType(ConfigurationParams), ReferenceType(PartialResultParams))) */ + workspace.configuration.WorkspaceConfigurationInput.reader given inputWriter: Writer[In] = - ??? + workspace.configuration.WorkspaceConfigurationInput.writer given outputWriter: Writer[Out] = vectorWriter[ujson.Value] diff --git a/modules/lsp/src/main/scala/generated/requests.scala b/modules/lsp/src/main/scala/generated/requests.scala index 24c536f9b..672d23802 100644 --- a/modules/lsp/src/main/scala/generated/requests.scala +++ b/modules/lsp/src/main/scala/generated/requests.scala @@ -733,9 +733,21 @@ object requests: * change event and empty the cache if such an event is received. */ object configuration extends LSPRequest("workspace/configuration") with codecs.requests_workspace_configuration: - type In = Any /*AndType(Vector(ReferenceType(ConfigurationParams), ReferenceType(PartialResultParams)))*/ + type In = WorkspaceConfigurationInput type Out = Vector[ujson.Value] + /** + * @param items + * @param partialResultToken + * An optional token that a server can use to report partial results (e.g. streaming) to + * the client. + + */ + case class WorkspaceConfigurationInput( + items: Vector[structures.ConfigurationItem], + partialResultToken: Opt[aliases.ProgressToken] = Opt.empty + ) + object WorkspaceConfigurationInput extends codecs.requests_workspace_configuration_WorkspaceConfigurationInput /** * The workspace diagnostic request definition. diff --git a/modules/lsp/src/main/scala/newtype.scala b/modules/lsp/src/main/scala/newtype.scala index 78e41020c..33a68413f 100644 --- a/modules/lsp/src/main/scala/newtype.scala +++ b/modules/lsp/src/main/scala/newtype.scala @@ -58,5 +58,7 @@ private[lsp] abstract class YesNo[A](using ev: Boolean =:= A): inline def apply(inline b: Boolean): A = ev.apply(b) - extension (inline a: A) inline def value: Boolean = a == Yes + extension (inline a: A) + inline def value: Boolean = a == Yes + inline def yes: Boolean = a == Yes end YesNo diff --git a/modules/lsp/src/test/scala/CodecTest.scala b/modules/lsp/src/test/scala/CodecTest.scala index 2a2248190..0b6e3d458 100644 --- a/modules/lsp/src/test/scala/CodecTest.scala +++ b/modules/lsp/src/test/scala/CodecTest.scala @@ -9,8 +9,8 @@ import cats.Monad import jsonrpclib.* -object CodecTest extends verify.BasicTestSuite: - test("test documentSymbol codec") { +class CodecTest() extends munit.FunSuite: + test("documentSymbol codec") { val out1 = Opt( Vector( @@ -56,4 +56,17 @@ object CodecTest extends verify.BasicTestSuite: assertEquals(read2.toString, out2.toString) } + + test("workspace/configuration codec (and types construction)") { + val req = workspace.configuration + val in = workspace.configuration.WorkspaceConfigurationInput( + items = Vector(ConfigurationItem(Opt("hello"))), + partialResultToken = Opt(ProgressToken("helllooooo")) + ) + + import req.WorkspaceConfigurationInput + + assertEquals(read[WorkspaceConfigurationInput](write(in)), in) + + } end CodecTest diff --git a/modules/lsp/src/test/scala/Test.scala b/modules/lsp/src/test/scala/LSPSuite.scala similarity index 97% rename from modules/lsp/src/test/scala/Test.scala rename to modules/lsp/src/test/scala/LSPSuite.scala index c113dbb19..e772abec3 100644 --- a/modules/lsp/src/test/scala/Test.scala +++ b/modules/lsp/src/test/scala/LSPSuite.scala @@ -12,7 +12,7 @@ def basicServer[F[_]: Monadic] = import jsonrpclib.* -object LSPSuite extends verify.BasicTestSuite: +class LSPSuite() extends munit.FunSuite: test("initialize") { import requests.* diff --git a/modules/meta/src/main/scala/Manager.scala b/modules/meta/src/main/scala/Manager.scala index 8ead2ccaf..85816c65d 100644 --- a/modules/meta/src/main/scala/Manager.scala +++ b/modules/meta/src/main/scala/Manager.scala @@ -40,5 +40,8 @@ class Manager(mm: MetaModel): notifications } - def get(s: String) = index.get(s) + def get( + s: String + ): Option[Enumeration | TypeAlias | Structure | Request | Notification] = + index.get(s) end Manager diff --git a/modules/meta/src/main/scala/newtype.scala b/modules/meta/src/main/scala/newtype.scala index cde6b7896..1fc067489 100644 --- a/modules/meta/src/main/scala/newtype.scala +++ b/modules/meta/src/main/scala/newtype.scala @@ -56,5 +56,8 @@ abstract class YesNo[A](using ev: Boolean =:= A): inline def apply(inline b: Boolean): A = ev.apply(b) - extension (inline a: A) inline def value: Boolean = a == Yes + extension (inline a: A) + inline def value: Boolean = a == Yes + inline def yes: Boolean = a == Yes + inline def no: Boolean = !yes end YesNo