From cb852eb3777c4b6027d02db77b1a28081aea4061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 22 Mar 2025 22:08:45 +0100 Subject: [PATCH 01/25] WIP: Langoustine adapter --- build.sbt | 10 ++ .../scala/playground/lsp/LanguageServer.scala | 20 ++- .../src/main/scala/playground/lsp2/Main.scala | 167 ++++++++++++++++++ 3 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 modules/lsp2/src/main/scala/playground/lsp2/Main.scala diff --git a/build.sbt b/build.sbt index a809cd0f5..a26206d01 100644 --- a/build.sbt +++ b/build.sbt @@ -232,6 +232,14 @@ lazy val lsp = module("lsp") ) .dependsOn(lspKernel) +lazy val lsp2 = module("lsp2") + .settings( + libraryDependencies ++= Seq( + "tech.neander" %% "langoustine-app" % "0.0.22" + ).pipe(jsoniterFix) + ) + .dependsOn(lspKernel) + lazy val e2e = module("e2e") .enablePlugins(BuildInfoPlugin) .settings( @@ -245,6 +253,7 @@ lazy val e2e = module("e2e") .dependsOn( lspKernel / publishLocal, lsp / publishLocal, + lsp2 / publishLocal, languageSupport / publishLocal, core / publishLocal, parser / publishLocal, @@ -284,6 +293,7 @@ lazy val root = project languageSupport, lspKernel, lsp, + lsp2, protocol4s, pluginCore, pluginSample, diff --git a/modules/lsp-kernel/src/main/scala/playground/lsp/LanguageServer.scala b/modules/lsp-kernel/src/main/scala/playground/lsp/LanguageServer.scala index 2c98e07f9..89e00758a 100644 --- a/modules/lsp-kernel/src/main/scala/playground/lsp/LanguageServer.scala +++ b/modules/lsp-kernel/src/main/scala/playground/lsp/LanguageServer.scala @@ -40,10 +40,9 @@ import playground.smithyql.SourceRange import playground.types.* import smithy4s.dynamic.DynamicSchemaIndex -// todo: move to kernel trait LanguageServer[F[_]] { - def initialize[A](workspaceFolders: List[Uri]): F[InitializeResult] + def initialize(workspaceFolders: List[Uri]): F[InitializeResult] def didChange( documentUri: Uri, @@ -146,7 +145,7 @@ object LanguageServer { getFormatterWidth ) - def initialize[A](workspaceFolders: List[Uri]): F[InitializeResult] = { + def initialize(workspaceFolders: List[Uri]): F[InitializeResult] = { def capabilities(compiler: ServerCapabilitiesCompiler): compiler.Result = { given Semigroup[compiler.Result] = compiler.semigroup compiler.textDocumentSync(TextDocumentSyncKind.Full) |+| @@ -345,6 +344,21 @@ trait ServerCapabilitiesCompiler { def documentSymbolProvider: Result } +object ServerCapabilitiesCompiler { + + abstract class Default extends ServerCapabilitiesCompiler { + def default: Result + + def textDocumentSync(kind: TextDocumentSyncKind): Result = default + def documentFormattingProvider: Result = default + def completionProvider: Result = default + def diagnosticProvider: Result = default + def codeLensProvider: Result = default + def documentSymbolProvider: Result = default + } + +} + case class ServerInfo(name: String, version: String) case class InitializeResult( diff --git a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala new file mode 100644 index 000000000..ad7635d16 --- /dev/null +++ b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala @@ -0,0 +1,167 @@ +package playground.lsp2 + +import cats.Functor +import cats.effect.IO +import cats.effect.kernel.Deferred +import cats.effect.kernel.Resource +import cats.kernel.Semigroup +import cats.syntax.all.* +import jsonrpclib.Channel +import jsonrpclib.Endpoint +import jsonrpclib.Monadic +import jsonrpclib.fs2.given +import langoustine.lsp.Communicate +import langoustine.lsp.Invocation +import langoustine.lsp.LSPBuilder +import langoustine.lsp.app.LangoustineApp +import langoustine.lsp.enumerations.MessageType +import langoustine.lsp.enumerations.TextDocumentSyncKind +import langoustine.lsp.requests.CustomNotification +import langoustine.lsp.requests.LSPNotification +import langoustine.lsp.requests.LSPRequest +import langoustine.lsp.requests.initialize +import langoustine.lsp.requests.window +import langoustine.lsp.requests.workspace +import langoustine.lsp.runtime.Opt +import langoustine.lsp.structures.CodeLensOptions +import langoustine.lsp.structures.CompletionOptions +import langoustine.lsp.structures.DiagnosticOptions +import langoustine.lsp.structures.InitializeResult +import langoustine.lsp.structures.InitializeResult.ServerInfo +import langoustine.lsp.structures.LogMessageParams +import langoustine.lsp.structures.ServerCapabilities +import langoustine.lsp.structures.ShowMessageParams +import playground.lsp.LanguageClient +import playground.lsp.MainServer +import playground.lsp.ServerCapabilitiesCompiler + +object Main extends LangoustineApp { + + def server(args: List[String]): Resource[IO, LSPBuilder[IO]] = IO + .deferred[LanguageClient[IO]] + .toResource + .flatMap { clientRef => + implicit val lc: LanguageClient[IO] = LanguageClient.defer(clientRef.get) + + MainServer + .makeServer[IO] + .map { server => + LSPBuilder + .create[IO] + .handleRequest(initialize) { req => + server + .initialize( + req.params.workspaceFolders.toOption.foldMap(_.toOption.orEmpty).toList.map { + workspaceFolder => + playground.language.Uri.fromUriString(workspaceFolder.uri.value) + } + ) + .map { result => + InitializeResult( + capabilities = result + .serverCapabilities(new ServerCapabilitiesCompiler.Default { + type Result = ServerCapabilities => ServerCapabilities + + def default: Result = identity + + def semigroup: Semigroup[ServerCapabilities => ServerCapabilities] = + _.andThen(_) + + // def codeLensProvider: Result = + // _.copy(codeLensProvider = Opt(CodeLensOptions())) + + // def completionProvider: Result = + // _.copy(completionProvider = Opt(CompletionOptions())) + + // def diagnosticProvider: Result = + // _.copy(diagnosticProvider = + // Opt( + // DiagnosticOptions( + // interFileDependencies = false, + // workspaceDiagnostics = false, + // ) + // ) + // ) + + // def documentFormattingProvider: Result = + // _.copy(documentFormattingProvider = Opt(true)) + + // def documentSymbolProvider: Result = + // _.copy(documentSymbolProvider = Opt(true)) + + override def textDocumentSync(kind: playground.lsp.TextDocumentSyncKind) + : Result = + _.copy(textDocumentSync = Opt(kind match { + case playground.lsp.TextDocumentSyncKind.Full => + TextDocumentSyncKind.Full + })) + + }) + .apply(ServerCapabilities()), + serverInfo = Opt( + ServerInfo( + name = result.serverInfo.name, + version = Opt(result.serverInfo.version), + ) + ), + ) + } + } + } + .map(bindClient(_, clientRef)) + } + + private def bindClient(lsp: LSPBuilder[IO], clientDef: Deferred[IO, LanguageClient[IO]]) + : LSPBuilder[IO] = + new LSPBuilder[IO] { + export lsp.build + export lsp.handleNotification + export lsp.handleRequest + + override def bind[T <: Channel[IO]]( + channel: T, + communicate: Communicate[IO], + )( + using Monadic[IO] + ): IO[T] = + clientDef.complete(ClientAdapter.adapt(communicate)) *> + lsp.bind(channel, communicate) + } + +} + +object ClientAdapter { + + def adapt[F[_]: Functor](comms: Communicate[F]): LanguageClient[F] = + new LanguageClient[F] { + def logOutput(msg: String): F[Unit] = comms.notification( + window.logMessage(LogMessageParams(`type` = MessageType.Info, message = msg)) + ) + + def configuration[A](v: playground.lsp.ConfigurationValue[A]): F[A] = ??? // for later + + def refreshCodeLenses: F[Unit] = comms.request(workspace.codeLens.refresh(())).void + def refreshDiagnostics: F[Unit] = comms.request(workspace.diagnostic.refresh(())).void + + def showMessage(tpe: playground.lsp.MessageType, msg: String): F[Unit] = comms.notification( + window.showMessage( + ShowMessageParams( + `type` = + tpe match { + case playground.lsp.MessageType.Error => MessageType.Error + case playground.lsp.MessageType.Info => MessageType.Info + case playground.lsp.MessageType.Warning => MessageType.Warning + }, + message = msg, + ) + ) + ) + + def showOutputPanel: F[Unit] = comms.notification(smithyql.showOutputPanel(())) + } + + object smithyql { + object showOutputPanel extends CustomNotification[Unit]("smithyql/showOutputPanel") + } + +} From d72fb1e344f02994ac438138c26182a5e77a56bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 22 Mar 2025 22:13:28 +0100 Subject: [PATCH 02/25] Split up into files --- .../lsp2/LangoustineClientAdapter.scala | 47 ++++++ .../lsp2/LangoustineServerAdapter.scala | 79 ++++++++++ .../src/main/scala/playground/lsp2/Main.scala | 138 ++---------------- 3 files changed, 138 insertions(+), 126 deletions(-) create mode 100644 modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala create mode 100644 modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala new file mode 100644 index 000000000..0e4a4db61 --- /dev/null +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala @@ -0,0 +1,47 @@ +package playground.lsp2 + +import cats.Functor +import cats.syntax.all.* +import langoustine.lsp.Communicate +import langoustine.lsp.enumerations.MessageType +import langoustine.lsp.requests.CustomNotification +import langoustine.lsp.requests.window +import langoustine.lsp.requests.workspace +import langoustine.lsp.structures.LogMessageParams +import langoustine.lsp.structures.ShowMessageParams + +object LangoustineClientAdapter { + + def adapt[F[_]: Functor](comms: Communicate[F]): playground.lsp.LanguageClient[F] = + new { + def logOutput(msg: String): F[Unit] = comms.notification( + window.logMessage(LogMessageParams(`type` = MessageType.Info, message = msg)) + ) + + def configuration[A](v: playground.lsp.ConfigurationValue[A]): F[A] = ??? // for later + + def refreshCodeLenses: F[Unit] = comms.request(workspace.codeLens.refresh(())).void + def refreshDiagnostics: F[Unit] = comms.request(workspace.diagnostic.refresh(())).void + + def showMessage(tpe: playground.lsp.MessageType, msg: String): F[Unit] = comms.notification( + window.showMessage( + ShowMessageParams( + `type` = + tpe match { + case playground.lsp.MessageType.Error => MessageType.Error + case playground.lsp.MessageType.Info => MessageType.Info + case playground.lsp.MessageType.Warning => MessageType.Warning + }, + message = msg, + ) + ) + ) + + def showOutputPanel: F[Unit] = comms.notification(smithyql.showOutputPanel(())) + } + + object smithyql { + object showOutputPanel extends CustomNotification[Unit]("smithyql/showOutputPanel") + } + +} diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala new file mode 100644 index 000000000..072279cbd --- /dev/null +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala @@ -0,0 +1,79 @@ +package playground.lsp2 + +import cats.Functor +import cats.kernel.Semigroup +import cats.syntax.all.* +import langoustine.lsp.LSPBuilder +import langoustine.lsp.enumerations.TextDocumentSyncKind +import langoustine.lsp.requests.initialize +import langoustine.lsp.runtime.Opt +import langoustine.lsp.structures.CodeLensOptions +import langoustine.lsp.structures.CompletionOptions +import langoustine.lsp.structures.DiagnosticOptions +import langoustine.lsp.structures.InitializeResult +import langoustine.lsp.structures.InitializeResult.ServerInfo +import langoustine.lsp.structures.ServerCapabilities +import playground.lsp.ServerCapabilitiesCompiler + +object LangoustineServerAdapter { + + def adapt[F[_]: Functor](server: playground.lsp.LanguageServer[F]) + : LSPBuilder[F] => LSPBuilder[F] = + _.handleRequest(initialize) { req => + server + .initialize( + req.params.workspaceFolders.toOption.foldMap(_.toOption.orEmpty).toList.map { + workspaceFolder => + playground.language.Uri.fromUriString(workspaceFolder.uri.value) + } + ) + .map { result => + InitializeResult( + capabilities = result + .serverCapabilities(new ServerCapabilitiesCompiler.Default { + type Result = ServerCapabilities => ServerCapabilities + + def default: Result = identity + + def semigroup: Semigroup[ServerCapabilities => ServerCapabilities] = _.andThen(_) + + // def codeLensProvider: Result = + // _.copy(codeLensProvider = Opt(CodeLensOptions())) + + // def completionProvider: Result = + // _.copy(completionProvider = Opt(CompletionOptions())) + + // def diagnosticProvider: Result = + // _.copy(diagnosticProvider = + // Opt( + // DiagnosticOptions( + // interFileDependencies = false, + // workspaceDiagnostics = false, + // ) + // ) + // ) + + // def documentFormattingProvider: Result = + // _.copy(documentFormattingProvider = Opt(true)) + + // def documentSymbolProvider: Result = + // _.copy(documentSymbolProvider = Opt(true)) + + override def textDocumentSync(kind: playground.lsp.TextDocumentSyncKind): Result = + _.copy(textDocumentSync = Opt(kind match { + case playground.lsp.TextDocumentSyncKind.Full => TextDocumentSyncKind.Full + })) + + }) + .apply(ServerCapabilities()), + serverInfo = Opt( + ServerInfo( + name = result.serverInfo.name, + version = Opt(result.serverInfo.version), + ) + ), + ) + } + } + +} diff --git a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala index ad7635d16..a785b1dbb 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala @@ -1,11 +1,8 @@ package playground.lsp2 -import cats.Functor import cats.effect.IO import cats.effect.kernel.Deferred import cats.effect.kernel.Resource -import cats.kernel.Semigroup -import cats.syntax.all.* import jsonrpclib.Channel import jsonrpclib.Endpoint import jsonrpclib.Monadic @@ -14,105 +11,30 @@ import langoustine.lsp.Communicate import langoustine.lsp.Invocation import langoustine.lsp.LSPBuilder import langoustine.lsp.app.LangoustineApp -import langoustine.lsp.enumerations.MessageType -import langoustine.lsp.enumerations.TextDocumentSyncKind -import langoustine.lsp.requests.CustomNotification import langoustine.lsp.requests.LSPNotification import langoustine.lsp.requests.LSPRequest -import langoustine.lsp.requests.initialize -import langoustine.lsp.requests.window -import langoustine.lsp.requests.workspace -import langoustine.lsp.runtime.Opt -import langoustine.lsp.structures.CodeLensOptions -import langoustine.lsp.structures.CompletionOptions -import langoustine.lsp.structures.DiagnosticOptions -import langoustine.lsp.structures.InitializeResult -import langoustine.lsp.structures.InitializeResult.ServerInfo -import langoustine.lsp.structures.LogMessageParams -import langoustine.lsp.structures.ServerCapabilities -import langoustine.lsp.structures.ShowMessageParams -import playground.lsp.LanguageClient -import playground.lsp.MainServer -import playground.lsp.ServerCapabilitiesCompiler object Main extends LangoustineApp { def server(args: List[String]): Resource[IO, LSPBuilder[IO]] = IO - .deferred[LanguageClient[IO]] + .deferred[playground.lsp.LanguageClient[IO]] .toResource .flatMap { clientRef => - implicit val lc: LanguageClient[IO] = LanguageClient.defer(clientRef.get) + implicit val lc + : playground.lsp.LanguageClient[IO] = playground.lsp.LanguageClient.defer(clientRef.get) - MainServer + playground + .lsp + .MainServer .makeServer[IO] - .map { server => - LSPBuilder - .create[IO] - .handleRequest(initialize) { req => - server - .initialize( - req.params.workspaceFolders.toOption.foldMap(_.toOption.orEmpty).toList.map { - workspaceFolder => - playground.language.Uri.fromUriString(workspaceFolder.uri.value) - } - ) - .map { result => - InitializeResult( - capabilities = result - .serverCapabilities(new ServerCapabilitiesCompiler.Default { - type Result = ServerCapabilities => ServerCapabilities - - def default: Result = identity - - def semigroup: Semigroup[ServerCapabilities => ServerCapabilities] = - _.andThen(_) - - // def codeLensProvider: Result = - // _.copy(codeLensProvider = Opt(CodeLensOptions())) - - // def completionProvider: Result = - // _.copy(completionProvider = Opt(CompletionOptions())) - - // def diagnosticProvider: Result = - // _.copy(diagnosticProvider = - // Opt( - // DiagnosticOptions( - // interFileDependencies = false, - // workspaceDiagnostics = false, - // ) - // ) - // ) - - // def documentFormattingProvider: Result = - // _.copy(documentFormattingProvider = Opt(true)) - - // def documentSymbolProvider: Result = - // _.copy(documentSymbolProvider = Opt(true)) - - override def textDocumentSync(kind: playground.lsp.TextDocumentSyncKind) - : Result = - _.copy(textDocumentSync = Opt(kind match { - case playground.lsp.TextDocumentSyncKind.Full => - TextDocumentSyncKind.Full - })) - - }) - .apply(ServerCapabilities()), - serverInfo = Opt( - ServerInfo( - name = result.serverInfo.name, - version = Opt(result.serverInfo.version), - ) - ), - ) - } - } - } + .map(LangoustineServerAdapter.adapt(_).apply(LSPBuilder.create[IO])) .map(bindClient(_, clientRef)) } - private def bindClient(lsp: LSPBuilder[IO], clientDef: Deferred[IO, LanguageClient[IO]]) - : LSPBuilder[IO] = + private def bindClient( + lsp: LSPBuilder[IO], + clientDef: Deferred[IO, playground.lsp.LanguageClient[IO]], + ): LSPBuilder[IO] = new LSPBuilder[IO] { export lsp.build export lsp.handleNotification @@ -124,44 +46,8 @@ object Main extends LangoustineApp { )( using Monadic[IO] ): IO[T] = - clientDef.complete(ClientAdapter.adapt(communicate)) *> + clientDef.complete(LangoustineClientAdapter.adapt(communicate)) *> lsp.bind(channel, communicate) } } - -object ClientAdapter { - - def adapt[F[_]: Functor](comms: Communicate[F]): LanguageClient[F] = - new LanguageClient[F] { - def logOutput(msg: String): F[Unit] = comms.notification( - window.logMessage(LogMessageParams(`type` = MessageType.Info, message = msg)) - ) - - def configuration[A](v: playground.lsp.ConfigurationValue[A]): F[A] = ??? // for later - - def refreshCodeLenses: F[Unit] = comms.request(workspace.codeLens.refresh(())).void - def refreshDiagnostics: F[Unit] = comms.request(workspace.diagnostic.refresh(())).void - - def showMessage(tpe: playground.lsp.MessageType, msg: String): F[Unit] = comms.notification( - window.showMessage( - ShowMessageParams( - `type` = - tpe match { - case playground.lsp.MessageType.Error => MessageType.Error - case playground.lsp.MessageType.Info => MessageType.Info - case playground.lsp.MessageType.Warning => MessageType.Warning - }, - message = msg, - ) - ) - ) - - def showOutputPanel: F[Unit] = comms.notification(smithyql.showOutputPanel(())) - } - - object smithyql { - object showOutputPanel extends CustomNotification[Unit]("smithyql/showOutputPanel") - } - -} From 24226ce94e3394b14007a10395c63ca4770fa19f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 22 Mar 2025 23:45:20 +0100 Subject: [PATCH 03/25] Pretty much a complete langoustine implementation --- build.sbt | 3 +- .../main/scala/playground/language/Uri.scala | 5 +- .../lsp2/LangoustineClientAdapter.scala | 31 +- .../lsp2/LangoustineServerAdapter.scala | 361 ++++++++++++++++-- .../playground/lsp2/ProtocolExtensions.scala | 17 + 5 files changed, 380 insertions(+), 37 deletions(-) create mode 100644 modules/lsp2/src/main/scala/playground/lsp2/ProtocolExtensions.scala diff --git a/build.sbt b/build.sbt index a26206d01..adb2fbc04 100644 --- a/build.sbt +++ b/build.sbt @@ -247,12 +247,11 @@ lazy val e2e = module("e2e") Seq[BuildInfoKey.Entry[_]]( // do you know how to simplify this? let me know please! Def .task { - s"""${(lsp / organization).value}::${(lsp / moduleName).value}:${(lsp / version).value}""" + s"""${(lsp2 / organization).value}::${(lsp2 / moduleName).value}:${(lsp2 / version).value}""" } // todo: replace with a full publishLocal before e2e in particular gets run (but not before tests run normally) .dependsOn( lspKernel / publishLocal, - lsp / publishLocal, lsp2 / publishLocal, languageSupport / publishLocal, core / publishLocal, diff --git a/modules/language-support/src/main/scala/playground/language/Uri.scala b/modules/language-support/src/main/scala/playground/language/Uri.scala index 1abbbe301..c7895bc8d 100644 --- a/modules/language-support/src/main/scala/playground/language/Uri.scala +++ b/modules/language-support/src/main/scala/playground/language/Uri.scala @@ -5,10 +5,7 @@ import fs2.io.file.Path import java.net.URI import java.nio.file.Paths -// not making constructor private because -// "access modifiers from `copy` method are copied from the case class constructor" -// cannot nicely be silenced in scala 2 -final case class Uri /* private */ ( +final case class Uri private ( value: String ) extends AnyVal { def toPath: Path = Path.fromNioPath(Paths.get(new URI(value))) diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala index 0e4a4db61..38f1b221f 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala @@ -1,24 +1,43 @@ package playground.lsp2 -import cats.Functor +import cats.MonadThrow import cats.syntax.all.* import langoustine.lsp.Communicate import langoustine.lsp.enumerations.MessageType -import langoustine.lsp.requests.CustomNotification import langoustine.lsp.requests.window import langoustine.lsp.requests.workspace +import langoustine.lsp.runtime.Opt +import langoustine.lsp.structures.ConfigurationItem +import langoustine.lsp.structures.ConfigurationParams import langoustine.lsp.structures.LogMessageParams import langoustine.lsp.structures.ShowMessageParams +import playground.lsp2.LangoustineServerAdapter.converters + +import ProtocolExtensions.smithyql object LangoustineClientAdapter { - def adapt[F[_]: Functor](comms: Communicate[F]): playground.lsp.LanguageClient[F] = + def adapt[F[_]: MonadThrow](comms: Communicate[F]): playground.lsp.LanguageClient[F] = new { def logOutput(msg: String): F[Unit] = comms.notification( window.logMessage(LogMessageParams(`type` = MessageType.Info, message = msg)) ) - def configuration[A](v: playground.lsp.ConfigurationValue[A]): F[A] = ??? // for later + def configuration[A](v: playground.lsp.ConfigurationValue[A]): F[A] = comms + .request( + workspace.configuration( + ConfigurationParams( + Vector( + ConfigurationItem( + section = Opt(v.key) + ) + ) + ) + ) + ) + .flatMap(_.headOption.liftTo[F](new Throwable("missing entry in the response"))) + .map(converters.fromLSP.json) + .flatMap(_.as[A](v.codec).liftTo[F]) def refreshCodeLenses: F[Unit] = comms.request(workspace.codeLens.refresh(())).void def refreshDiagnostics: F[Unit] = comms.request(workspace.diagnostic.refresh(())).void @@ -40,8 +59,4 @@ object LangoustineClientAdapter { def showOutputPanel: F[Unit] = comms.notification(smithyql.showOutputPanel(())) } - object smithyql { - object showOutputPanel extends CustomNotification[Unit]("smithyql/showOutputPanel") - } - } diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala index 072279cbd..beacce78e 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala @@ -1,23 +1,57 @@ package playground.lsp2 -import cats.Functor +import cats.Applicative +import cats.ApplicativeThrow import cats.kernel.Semigroup +import cats.parse.LocationMap import cats.syntax.all.* +import io.circe.Json import langoustine.lsp.LSPBuilder +import langoustine.lsp.aliases.DocumentDiagnosticReport +import langoustine.lsp.aliases.TextDocumentContentChangeEvent +import langoustine.lsp.enumerations.CompletionItemKind +import langoustine.lsp.enumerations.InsertTextFormat +import langoustine.lsp.enumerations.MarkupKind import langoustine.lsp.enumerations.TextDocumentSyncKind +import langoustine.lsp.requests.exit import langoustine.lsp.requests.initialize +import langoustine.lsp.requests.shutdown +import langoustine.lsp.requests.textDocument +import langoustine.lsp.requests.workspace import langoustine.lsp.runtime.Opt +import langoustine.lsp.structures.CodeLens import langoustine.lsp.structures.CodeLensOptions +import langoustine.lsp.structures.CompletionItem import langoustine.lsp.structures.CompletionOptions +import langoustine.lsp.structures.Diagnostic import langoustine.lsp.structures.DiagnosticOptions +import langoustine.lsp.structures.DocumentSymbol +import langoustine.lsp.structures.FullDocumentDiagnosticReport import langoustine.lsp.structures.InitializeResult import langoustine.lsp.structures.InitializeResult.ServerInfo +import langoustine.lsp.structures.MarkupContent +import langoustine.lsp.structures.Position +import langoustine.lsp.structures.RelatedFullDocumentDiagnosticReport import langoustine.lsp.structures.ServerCapabilities +import langoustine.lsp.structures.TextEdit +import playground.CompilationError +import playground.language.InsertText +import playground.language.Uri +import playground.lsp.LSPCodeLens +import playground.lsp.LSPCompletionItem +import playground.lsp.LSPDiagnostic +import playground.lsp.LSPDocumentSymbol +import playground.lsp.LSPPosition +import playground.lsp.LSPRange +import playground.lsp.LSPTextEdit +import playground.lsp.RunFileParams import playground.lsp.ServerCapabilitiesCompiler +import playground.lsp2.ProtocolExtensions.smithyql +import playground.smithyql.SourceRange object LangoustineServerAdapter { - def adapt[F[_]: Functor](server: playground.lsp.LanguageServer[F]) + def adapt[F[_]: ApplicativeThrow](server: playground.lsp.LanguageServer[F]) : LSPBuilder[F] => LSPBuilder[F] = _.handleRequest(initialize) { req => server @@ -30,36 +64,32 @@ object LangoustineServerAdapter { .map { result => InitializeResult( capabilities = result - .serverCapabilities(new ServerCapabilitiesCompiler.Default { + .serverCapabilities(new ServerCapabilitiesCompiler { type Result = ServerCapabilities => ServerCapabilities - def default: Result = identity - def semigroup: Semigroup[ServerCapabilities => ServerCapabilities] = _.andThen(_) - // def codeLensProvider: Result = - // _.copy(codeLensProvider = Opt(CodeLensOptions())) + def codeLensProvider: Result = _.copy(codeLensProvider = Opt(CodeLensOptions())) - // def completionProvider: Result = - // _.copy(completionProvider = Opt(CompletionOptions())) + def completionProvider: Result = + _.copy(completionProvider = Opt(CompletionOptions())) - // def diagnosticProvider: Result = - // _.copy(diagnosticProvider = - // Opt( - // DiagnosticOptions( - // interFileDependencies = false, - // workspaceDiagnostics = false, - // ) - // ) - // ) + def diagnosticProvider: Result = + _.copy(diagnosticProvider = + Opt( + DiagnosticOptions( + interFileDependencies = false, + workspaceDiagnostics = false, + ) + ) + ) - // def documentFormattingProvider: Result = - // _.copy(documentFormattingProvider = Opt(true)) + def documentFormattingProvider: Result = + _.copy(documentFormattingProvider = Opt(true)) - // def documentSymbolProvider: Result = - // _.copy(documentSymbolProvider = Opt(true)) + def documentSymbolProvider: Result = _.copy(documentSymbolProvider = Opt(true)) - override def textDocumentSync(kind: playground.lsp.TextDocumentSyncKind): Result = + def textDocumentSync(kind: playground.lsp.TextDocumentSyncKind): Result = _.copy(textDocumentSync = Opt(kind match { case playground.lsp.TextDocumentSyncKind.Full => TextDocumentSyncKind.Full })) @@ -75,5 +105,290 @@ object LangoustineServerAdapter { ) } } + // todo: are notifications handled fire-and-forget? or do we need a dispatcher? + .handleNotification(textDocument.didChange) { req => + req + .params + .contentChanges + .headOption + .traverse_ { change => + change match { + case _: TextDocumentContentChangeEvent.S0 => + new Exception("Unexpected incremental text change event").raiseError + + case TextDocumentContentChangeEvent.S1(newText) => + server.didChange( + documentUri = converters.fromLSP.uri(req.params.textDocument.uri), + newText = newText, + ) + } + } + } + .handleNotification(textDocument.didOpen) { req => + server.didOpen( + documentUri = converters.fromLSP.uri(req.params.textDocument.uri), + text = req.params.textDocument.text, + ) + } + .handleNotification(textDocument.didSave) { req => + server.didSave(converters.fromLSP.uri(req.params.textDocument.uri)) + } + .handleNotification(textDocument.didClose) { req => + server.didClose(converters.fromLSP.uri(req.params.textDocument.uri)) + } + .handleRequest(textDocument.formatting) { req => + server + .formatting(documentUri = converters.fromLSP.uri(req.params.textDocument.uri)) + .map(_.map(converters.toLSP.textEdit)) + .map(edits => Opt(edits.toVector)) + } + .handleRequest(textDocument.completion) { req => + server + .completion( + documentUri = converters.fromLSP.uri(req.params.textDocument.uri), + position = converters.fromLSP.position(req.params.position), + ) + .map(_.map(converters.toLSP.completionItem)) + .map(items => Opt(items.toVector)) + } + .handleRequest(textDocument.diagnostic) { req => + server + .diagnostic(documentUri = converters.fromLSP.uri(req.params.textDocument.uri)) + .map(_.map(converters.toLSP.diagnostic)) + .map(diags => + DocumentDiagnosticReport( + RelatedFullDocumentDiagnosticReport( + kind = "full", + items = diags.toVector, + ) + ) + ) + } + .handleRequest(textDocument.codeLens) { req => + server + .codeLens(documentUri = converters.fromLSP.uri(req.params.textDocument.uri)) + .map(_.map(converters.toLSP.codeLens)) + .map(edits => Opt(edits.toVector)) + } + .handleRequest(workspace.executeCommand) { req => + server + .executeCommand( + commandName = req.params.command, + arguments = req.params.arguments.toOption.orEmpty.toList.map(converters.fromLSP.json), + ) + .as(Opt.empty) + } + .handleNotification(workspace.didChangeWatchedFiles)(_ => server.didChangeWatchedFiles) + .handleRequest(textDocument.documentSymbol) { req => + server + .documentSymbol(documentUri = converters.fromLSP.uri(req.params.textDocument.uri)) + .map(_.map(converters.toLSP.documentSymbol)) + .map(symbols => Opt(symbols.toVector)) + } + .handleRequest(smithyql.runQuery) { req => + server.runFile(RunFileParams(converters.fromLSP.uri(req.params.uri))) + } + .handleNotification(exit)(_ => Applicative[F].unit) + .handleRequest(shutdown)(_ => Applicative[F].pure(null: shutdown.Out /* Anton wtf */ )) + + object converters { + + object fromLSP { + def uri(uri: langoustine.lsp.runtime.DocumentUri) + : Uri = playground.language.Uri.fromUriString(uri.value) + + def json(u: ujson.Value): Json = + u match { + case ujson.Null => Json.Null + case ujson.True => Json.True + case ujson.False => Json.False + case ujson.Num(n) => Json.fromDoubleOrNull(n) + case ujson.Str(s) => Json.fromString(s) + case ujson.Arr(arr) => Json.fromValues(arr.map(json)) + case ujson.Obj(obj) => Json.fromFields(obj.map { case (k, v) => k -> json(v) }) + } + + def position(pos: Position): LSPPosition = LSPPosition( + line = pos.line.value, + character = pos.character.value, + ) + + } + + object toLSP { + + def completionItem(item: LSPCompletionItem): CompletionItem = completionItem( + item.item, + item.map, + ) + + private def completionItem(item: playground.language.CompletionItem, map: LocationMap) + : CompletionItem = { + val convertKind: playground.language.CompletionItemKind => CompletionItemKind = { + case playground.language.CompletionItemKind.EnumMember => CompletionItemKind.EnumMember + case playground.language.CompletionItemKind.Field => CompletionItemKind.Field + case playground.language.CompletionItemKind.Constant => CompletionItemKind.Constant + case playground.language.CompletionItemKind.UnionMember => CompletionItemKind.Class + case playground.language.CompletionItemKind.Function => CompletionItemKind.Function + case playground.language.CompletionItemKind.Module => CompletionItemKind.Module + } + + val insertText = + item.insertText match { + case InsertText.JustString(value) => value + case InsertText.SnippetString(value) => value + } + + val insertTextFmt = + item.insertText match { + case _: InsertText.JustString => InsertTextFormat.PlainText + case _: InsertText.SnippetString => InsertTextFormat.Snippet + } + + val additionalTextEdits: List[TextEdit] = item + .extraTextEdits + .map(textEdit(_, map)) + + CompletionItem( + label = item.label, + labelDetails = Opt( + langoustine + .lsp + .structures + .CompletionItemLabelDetails( + detail = Opt(item.detail) + ) + ), + detail = Opt.fromOption(item.description), + kind = Opt(convertKind(item.kind)), + insertText = Opt(insertText), + insertTextFormat = Opt(insertTextFmt), + additionalTextEdits = Opt(additionalTextEdits.toVector), + tags = Opt { + if item.deprecated then Vector( + langoustine.lsp.enumerations.CompletionItemTag.Deprecated + ) + else + Vector.empty + }, + documentation = Opt.fromOption(item.docs.map { docs => + MarkupContent( + kind = MarkupKind.Markdown, + value = docs, + ) + }), + sortText = Opt.fromOption(item.sortText), + ) + } + + def codeLens(lens: LSPCodeLens): CodeLens = codeLens(lens.lens, lens.map) + + def codeLens(lens: playground.language.CodeLens, map: LocationMap): CodeLens = CodeLens( + range = range(LSPRange.from(lens.range, map)), + command = Opt { + langoustine + .lsp + .structures + .Command( + title = lens.command.title, + command = lens.command.command, + arguments = Opt(lens.command.args.map(ujson.Str(_)).toVector), + ) + }, + ) + + def textEdit(edit: LSPTextEdit): TextEdit = textEdit(edit.textEdit, edit.map) + + private def textEdit(edit: playground.language.TextEdit, map: LocationMap): TextEdit = + edit match { + case playground.language.TextEdit.Insert(what, where) => + TextEdit( + range = toLSP.range(LSPRange.from(SourceRange(where, where), map)), + newText = what, + ) + + case playground.language.TextEdit.Overwrite(what, range) => + TextEdit( + range = toLSP.range(LSPRange.from(range, map)), + newText = what, + ) + } + + def diagnostic(diag: LSPDiagnostic): Diagnostic = diagnostic(diag.diagnostic, diag.map) + + private def diagnostic(diag: CompilationError, map: LocationMap): Diagnostic = Diagnostic( + range = toLSP.range(LSPRange.from(diag.range, map)), + message = diag.err.render, + severity = Opt { + diag.severity match { + case playground.DiagnosticSeverity.Error => + langoustine.lsp.enumerations.DiagnosticSeverity.Error + case playground.DiagnosticSeverity.Information => + langoustine.lsp.enumerations.DiagnosticSeverity.Information + case playground.DiagnosticSeverity.Warning => + langoustine.lsp.enumerations.DiagnosticSeverity.Warning + } + }, + tags = Opt( + diag + .tags + .map { + case playground.DiagnosticTag.Deprecated => + langoustine.lsp.enumerations.DiagnosticTag.Deprecated + case playground.DiagnosticTag.Unused => + langoustine.lsp.enumerations.DiagnosticTag.Unnecessary + } + .toVector + ), + ) + + def documentSymbol(sym: LSPDocumentSymbol): DocumentSymbol = documentSymbol(sym.sym, sym.map) + + private def documentSymbol(sym: playground.language.DocumentSymbol, map: LocationMap) + : DocumentSymbol = DocumentSymbol( + name = sym.name, + kind = + sym.kind match { + case playground.language.SymbolKind.Array => + langoustine.lsp.enumerations.SymbolKind.Array + case playground.language.SymbolKind.Field => + langoustine.lsp.enumerations.SymbolKind.Field + case playground.language.SymbolKind.Function => + langoustine.lsp.enumerations.SymbolKind.Function + case playground.language.SymbolKind.Package => + langoustine.lsp.enumerations.SymbolKind.Package + }, + range = converters + .toLSP + .range( + LSPRange.from(sym.range, map) + ), + selectionRange = converters + .toLSP + .range( + LSPRange.from(sym.selectionRange, map) + ), + children = Opt(sym.children.map(documentSymbol(_, map)).toVector), + ) + + def range(range: LSPRange): langoustine.lsp.structures.Range = langoustine + .lsp + .structures + .Range( + start = position(range.from), + end = position(range.to), + ) + + def position(position: LSPPosition): langoustine.lsp.structures.Position = langoustine + .lsp + .structures + .Position( + line = position.line, + character = position.character, + ) + + } + + } } diff --git a/modules/lsp2/src/main/scala/playground/lsp2/ProtocolExtensions.scala b/modules/lsp2/src/main/scala/playground/lsp2/ProtocolExtensions.scala new file mode 100644 index 000000000..4c48613a1 --- /dev/null +++ b/modules/lsp2/src/main/scala/playground/lsp2/ProtocolExtensions.scala @@ -0,0 +1,17 @@ +package playground.lsp2 + +import langoustine.lsp.requests.CustomNotification +import langoustine.lsp.requests.CustomRequest +import langoustine.lsp.runtime.DocumentUri +import upickle.default.* + +object ProtocolExtensions { + + object smithyql { + object showOutputPanel extends CustomNotification[Unit]("smithyql/showOutputPanel") + object runQuery extends CustomRequest[RunQueryParams, Unit]("smithyql/runQuery") + + case class RunQueryParams(uri: DocumentUri) derives ReadWriter + } + +} From 04bc090943820985f9ee6a024d631cff603442f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 23 Mar 2025 02:11:25 +0100 Subject: [PATCH 04/25] WIP --- build.sbt | 4 +- .../test/scala/playground/e2e/E2ETests.scala | 130 +++++++++++++++++- .../lsp2/LangoustineServerAdapter.scala | 10 +- 3 files changed, 133 insertions(+), 11 deletions(-) diff --git a/build.sbt b/build.sbt index adb2fbc04..2c5cd18dd 100644 --- a/build.sbt +++ b/build.sbt @@ -235,7 +235,7 @@ lazy val lsp = module("lsp") lazy val lsp2 = module("lsp2") .settings( libraryDependencies ++= Seq( - "tech.neander" %% "langoustine-app" % "0.0.22" + "tech.neander" %% "langoustine-app" % "0.0.22+2-9b9fac05+20250323-0105-SNAPSHOT" ).pipe(jsoniterFix) ) .dependsOn(lspKernel) @@ -268,7 +268,7 @@ lazy val e2e = module("e2e") publish / skip := true, Test / fork := true, ) - .dependsOn(lsp) + .dependsOn(lsp, lsp2) val writeVersion = taskKey[Unit]("Writes the current version to the `.version` file") diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 9592492d5..15ddadee4 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -6,6 +6,13 @@ import cats.effect.kernel.Resource import cats.effect.unsafe.implicits.* import cats.syntax.all.* import fs2.io.file +import jsonrpclib.fs2.FS2Channel +import jsonrpclib.fs2.given +import langoustine.lsp.Communicate +import langoustine.lsp.requests.initialize +import langoustine.lsp.runtime.Opt +import langoustine.lsp.runtime.Uri +import org.eclipse.lsp4j.ClientCapabilities import org.eclipse.lsp4j.InitializeParams import org.eclipse.lsp4j.InitializeResult import org.eclipse.lsp4j.MessageActionItem @@ -29,7 +36,7 @@ import scala.util.chaining.* object E2ETests extends SimpleIOSuite { class LanguageServerAdapter( - ls: LanguageServer + val ls: LanguageServer ) { def initialize( @@ -38,6 +45,70 @@ object E2ETests extends SimpleIOSuite { } + private def runServer2: Resource[IO, Communicate[IO]] = + // val client: PlaygroundLanguageClient = + // new PlaygroundLanguageClient { + + // override def telemetryEvent( + // `object`: Object + // ): Unit = () + + // override def publishDiagnostics( + // diagnostics: PublishDiagnosticsParams + // ): Unit = () + + // override def showMessage( + // messageParams: MessageParams + // ): Unit = println( + // s"${Console.MAGENTA}Message from server: ${messageParams + // .getMessage()} (type: ${messageParams.getType()})${Console.RESET}" + // ) + + // override def showMessageRequest( + // requestParams: ShowMessageRequestParams + // ): CompletableFuture[MessageActionItem] = (IO.stub: IO[MessageActionItem]) + // .unsafeToCompletableFuture() + + // override def logMessage( + // message: MessageParams + // ): Unit = () + + // override def showOutputPanel( + // ): Unit = () + + // } + + // val builder = + // new ProcessBuilder( + // "cs", + // "launch", + // BuildInfo.lspArtifact, + // ) + // // Watch process stderr in test runner + // .redirectError(Redirect.INHERIT) + + // Resource + // .make(IO.interruptibleMany(builder.start()))(p => IO(p.destroy()).void) + // .flatMap { process => + // val launcher = new LSPLauncher.Builder[LanguageServer]() + // .setLocalService(client) + // .setRemoteInterface(classOf[LanguageServer]) + // .setInput(process.getInputStream()) + // .setOutput(process.getOutputStream()) + // .traceMessages(new PrintWriter(System.err)) + // .create() + + // Resource + // .make(IO(launcher.startListening()).timeout(5.seconds))(f => + // IO(f.cancel(true): @nowarn("msg=discarded non-Unit")) + // ) + // .as(new LanguageServerAdapter(launcher.getRemoteProxy())) + // } + + FS2Channel[IO]().compile.resource.onlyOrError.map { channel => + Communicate.channel(channel) + } + private def runServer: Resource[IO, LanguageServerAdapter] = { val client: PlaygroundLanguageClient = @@ -113,22 +184,67 @@ object E2ETests extends SimpleIOSuite { .asJava ) ) + .tap(_.setCapabilities(new ClientCapabilities())) - test("server startup and initialize") { - runServer + private def initializeParams2( + workspaceFolders: List[file.Path] + ): langoustine.lsp.structures.InitializeParams = langoustine + .lsp + .structures + .InitializeParams( + processId = Opt.empty, + rootUri = Opt.empty, + capabilities = langoustine.lsp.structures.ClientCapabilities(), + workspaceFolders = Opt( + Opt( + workspaceFolders + .zipWithIndex + .map { case (path, i) => + langoustine + .lsp + .structures + .WorkspaceFolder( + uri = Uri(path.toNioPath.toUri().toString), + name = s"test-workspace-$i", + ) + } + .toVector + ) + ), + ) + + test("server startup and initialize 2") { + runServer2 .use { ls => file.Files[IO].tempDirectory.use { tempDirectory => - val initParams = initializeParams(workspaceFolders = List(tempDirectory)) + val initParams = initializeParams2(workspaceFolders = List(tempDirectory)) - ls.initialize(initParams).map { result => + ls.request(initialize(initParams)).map { result => assert.eql( - result.getServerInfo().getName(), + result.serverInfo.toOption.get.name, "Smithy Playground", ) } } - } .timeout(20.seconds) } + + // test("server startup and initialize") { + // runServer + // .use { ls => + // file.Files[IO].tempDirectory.use { tempDirectory => + // val initParams = initializeParams(workspaceFolders = List(tempDirectory)) + + // ls.initialize(initParams).map { result => + // assert.eql( + // result.getServerInfo().getName(), + // "Smithy Playground", + // ) + // } <* IO(ls.ls.exit()) + // } + + // } + // .timeout(20.seconds) + // } } diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala index beacce78e..2e88418d5 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala @@ -188,8 +188,14 @@ object LangoustineServerAdapter { .handleRequest(smithyql.runQuery) { req => server.runFile(RunFileParams(converters.fromLSP.uri(req.params.uri))) } - .handleNotification(exit)(_ => Applicative[F].unit) - .handleRequest(shutdown)(_ => Applicative[F].pure(null: shutdown.Out /* Anton wtf */ )) + .handleNotification(exit) { _ => + println("we're in an exit now") + Applicative[F].unit + } + .handleRequest(shutdown) { _ => + println("we're in a shutdown now") + Applicative[F].pure(null: shutdown.Out /* Anton wtf */ ) + } object converters { From 76b83f34177250efd3f28efd84b434dabdaacba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Tue, 13 May 2025 23:36:46 +0200 Subject: [PATCH 05/25] Update to langoustine 0.0.23 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index c381634ea..2bd652985 100644 --- a/build.sbt +++ b/build.sbt @@ -235,7 +235,7 @@ lazy val lsp = module("lsp") lazy val lsp2 = module("lsp2") .settings( libraryDependencies ++= Seq( - "tech.neander" %% "langoustine-app" % "0.0.22+2-9b9fac05+20250323-0105-SNAPSHOT" + "tech.neander" %% "langoustine-app" % "0.0.23" ).pipe(jsoniterFix) ) .dependsOn(lspKernel) From f6a7af1b0fa0da8ca61f52e73e76408bbd30e101 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 14 May 2025 02:04:19 +0200 Subject: [PATCH 06/25] Move to langoustine in e2e tests, use snapshot with deadlock fix --- build.sbt | 2 +- .../test/scala/playground/e2e/E2ETests.scala | 209 +++++------------- .../lsp2/LangoustineServerAdapter.scala | 6 +- .../src/main/scala/playground/lsp2/Main.scala | 2 - 4 files changed, 55 insertions(+), 164 deletions(-) diff --git a/build.sbt b/build.sbt index 2bd652985..fdcfe3931 100644 --- a/build.sbt +++ b/build.sbt @@ -235,7 +235,7 @@ lazy val lsp = module("lsp") lazy val lsp2 = module("lsp2") .settings( libraryDependencies ++= Seq( - "tech.neander" %% "langoustine-app" % "0.0.23" + "tech.neander" %% "langoustine-app" % "0.0.23+3-8cfea919-SNAPSHOT" ).pipe(jsoniterFix) ) .dependsOn(lspKernel) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 15ddadee4..666d7b4e7 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -1,15 +1,19 @@ package playground.e2e import buildinfo.BuildInfo +import cats.effect.Concurrent import cats.effect.IO import cats.effect.kernel.Resource import cats.effect.unsafe.implicits.* import cats.syntax.all.* import fs2.io.file +import fs2.io.process.Processes import jsonrpclib.fs2.FS2Channel import jsonrpclib.fs2.given import langoustine.lsp.Communicate +import langoustine.lsp.LSPBuilder import langoustine.lsp.requests.initialize +import langoustine.lsp.requests.window import langoustine.lsp.runtime.Opt import langoustine.lsp.runtime.Uri import org.eclipse.lsp4j.ClientCapabilities @@ -22,6 +26,7 @@ import org.eclipse.lsp4j.ShowMessageRequestParams import org.eclipse.lsp4j.WorkspaceFolder import org.eclipse.lsp4j.launch.LSPLauncher import org.eclipse.lsp4j.services.LanguageServer +import playground.e2e.E2ETests.LanguageServerAdapter import playground.lsp.PlaygroundLanguageClient import weaver.* @@ -45,149 +50,55 @@ object E2ETests extends SimpleIOSuite { } - private def runServer2: Resource[IO, Communicate[IO]] = - // val client: PlaygroundLanguageClient = - // new PlaygroundLanguageClient { - - // override def telemetryEvent( - // `object`: Object - // ): Unit = () - - // override def publishDiagnostics( - // diagnostics: PublishDiagnosticsParams - // ): Unit = () - - // override def showMessage( - // messageParams: MessageParams - // ): Unit = println( - // s"${Console.MAGENTA}Message from server: ${messageParams - // .getMessage()} (type: ${messageParams.getType()})${Console.RESET}" - // ) - - // override def showMessageRequest( - // requestParams: ShowMessageRequestParams - // ): CompletableFuture[MessageActionItem] = (IO.stub: IO[MessageActionItem]) - // .unsafeToCompletableFuture() - - // override def logMessage( - // message: MessageParams - // ): Unit = () - - // override def showOutputPanel( - // ): Unit = () - - // } - - // val builder = - // new ProcessBuilder( - // "cs", - // "launch", - // BuildInfo.lspArtifact, - // ) - // // Watch process stderr in test runner - // .redirectError(Redirect.INHERIT) - - // Resource - // .make(IO.interruptibleMany(builder.start()))(p => IO(p.destroy()).void) - // .flatMap { process => - // val launcher = new LSPLauncher.Builder[LanguageServer]() - // .setLocalService(client) - // .setRemoteInterface(classOf[LanguageServer]) - // .setInput(process.getInputStream()) - // .setOutput(process.getOutputStream()) - // .traceMessages(new PrintWriter(System.err)) - // .create() - - // Resource - // .make(IO(launcher.startListening()).timeout(5.seconds))(f => - // IO(f.cancel(true): @nowarn("msg=discarded non-Unit")) - // ) - // .as(new LanguageServerAdapter(launcher.getRemoteProxy())) - // } + private def runServer2: Resource[IO, Communicate[IO]] = Processes[IO] + .spawn(fs2.io.process.ProcessBuilder("cs", "launch", BuildInfo.lspArtifact)) + .flatMap { process => + val clientEndpoints: LSPBuilder[IO] => LSPBuilder[IO] = + _.handleNotification(window.showMessage) { in => + val messageParams = in.params + IO.println { + s"${Console.MAGENTA}Message from server: ${messageParams.message} (type: ${messageParams.`type`})${Console.RESET}" + } + } - FS2Channel[IO]().compile.resource.onlyOrError.map { channel => - Communicate.channel(channel) + FS2Channel[IO]() + .compile + .resource + .onlyOrError + .flatMap { chan => + val comms = Communicate.channel(chan) + chan + .withEndpoints(clientEndpoints(LSPBuilder.create[IO]).build(comms)) + .flatMap { channel => + fs2 + .Stream + .never[IO] + .concurrently( + process + .stdout + // fs2.io.stdout seems to be printed repeatedly for some reason, could be a bug with sbt + .observe(_.through(fs2.text.utf8.decode[IO]).debug("stdout: " + _).drain) + .through(jsonrpclib.fs2.lsp.decodeMessages[IO]) + .through(channel.inputOrBounce) + ) + .concurrently( + channel + .output + .through(jsonrpclib.fs2.lsp.encodeMessages[IO]) + .observe(_.through(fs2.text.utf8.decode[IO]).debug("stdin: " + _).drain) + .through(process.stdin) + ) + .concurrently(process.stderr.through(fs2.io.stderr[IO])) + .compile + .drain + .background + .as(comms) + } + } } - private def runServer: Resource[IO, LanguageServerAdapter] = { - - val client: PlaygroundLanguageClient = - new PlaygroundLanguageClient { - - override def telemetryEvent( - `object`: Object - ): Unit = () - - override def publishDiagnostics( - diagnostics: PublishDiagnosticsParams - ): Unit = () - - override def showMessage( - messageParams: MessageParams - ): Unit = println( - s"${Console.MAGENTA}Message from server: ${messageParams - .getMessage()} (type: ${messageParams.getType()})${Console.RESET}" - ) - - override def showMessageRequest( - requestParams: ShowMessageRequestParams - ): CompletableFuture[MessageActionItem] = (IO.stub: IO[MessageActionItem]) - .unsafeToCompletableFuture() - - override def logMessage( - message: MessageParams - ): Unit = () - - override def showOutputPanel( - ): Unit = () - - } - - val builder = - new ProcessBuilder( - "cs", - "launch", - BuildInfo.lspArtifact, - ) - // Watch process stderr in test runner - .redirectError(Redirect.INHERIT) - - Resource - .make(IO.interruptibleMany(builder.start()))(p => IO(p.destroy()).void) - .flatMap { process => - val launcher = new LSPLauncher.Builder[LanguageServer]() - .setLocalService(client) - .setRemoteInterface(classOf[LanguageServer]) - .setInput(process.getInputStream()) - .setOutput(process.getOutputStream()) - .traceMessages(new PrintWriter(System.err)) - .create() - - Resource - .make(IO(launcher.startListening()).timeout(5.seconds))(f => - IO(f.cancel(true): @nowarn("msg=discarded non-Unit")) - ) - .as(new LanguageServerAdapter(launcher.getRemoteProxy())) - } - } - private def initializeParams( workspaceFolders: List[file.Path] - ): InitializeParams = new InitializeParams() - .tap( - _.setWorkspaceFolders( - workspaceFolders - .zipWithIndex - .map { case (path, i) => - new WorkspaceFolder(path.toNioPath.toUri().toString(), s"test-workspace-$i") - } - .asJava - ) - ) - .tap(_.setCapabilities(new ClientCapabilities())) - - private def initializeParams2( - workspaceFolders: List[file.Path] ): langoustine.lsp.structures.InitializeParams = langoustine .lsp .structures @@ -213,11 +124,11 @@ object E2ETests extends SimpleIOSuite { ), ) - test("server startup and initialize 2") { + test("server startup and initialize") { runServer2 .use { ls => file.Files[IO].tempDirectory.use { tempDirectory => - val initParams = initializeParams2(workspaceFolders = List(tempDirectory)) + val initParams = initializeParams(workspaceFolders = List(tempDirectory)) ls.request(initialize(initParams)).map { result => assert.eql( @@ -229,22 +140,4 @@ object E2ETests extends SimpleIOSuite { } .timeout(20.seconds) } - - // test("server startup and initialize") { - // runServer - // .use { ls => - // file.Files[IO].tempDirectory.use { tempDirectory => - // val initParams = initializeParams(workspaceFolders = List(tempDirectory)) - - // ls.initialize(initParams).map { result => - // assert.eql( - // result.getServerInfo().getName(), - // "Smithy Playground", - // ) - // } <* IO(ls.ls.exit()) - // } - - // } - // .timeout(20.seconds) - // } } diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala index 2e88418d5..b44cddd5e 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala @@ -26,7 +26,6 @@ import langoustine.lsp.structures.CompletionOptions import langoustine.lsp.structures.Diagnostic import langoustine.lsp.structures.DiagnosticOptions import langoustine.lsp.structures.DocumentSymbol -import langoustine.lsp.structures.FullDocumentDiagnosticReport import langoustine.lsp.structures.InitializeResult import langoustine.lsp.structures.InitializeResult.ServerInfo import langoustine.lsp.structures.MarkupContent @@ -54,6 +53,7 @@ object LangoustineServerAdapter { def adapt[F[_]: ApplicativeThrow](server: playground.lsp.LanguageServer[F]) : LSPBuilder[F] => LSPBuilder[F] = _.handleRequest(initialize) { req => + System.err.println("got initialize request") server .initialize( req.params.workspaceFolders.toOption.foldMap(_.toOption.orEmpty).toList.map { @@ -189,11 +189,11 @@ object LangoustineServerAdapter { server.runFile(RunFileParams(converters.fromLSP.uri(req.params.uri))) } .handleNotification(exit) { _ => - println("we're in an exit now") + System.err.println("we're in an exit now") Applicative[F].unit } .handleRequest(shutdown) { _ => - println("we're in a shutdown now") + System.err.println("we're in a shutdown now") Applicative[F].pure(null: shutdown.Out /* Anton wtf */ ) } diff --git a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala index a785b1dbb..bbc1b1d84 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala @@ -4,11 +4,9 @@ import cats.effect.IO import cats.effect.kernel.Deferred import cats.effect.kernel.Resource import jsonrpclib.Channel -import jsonrpclib.Endpoint import jsonrpclib.Monadic import jsonrpclib.fs2.given import langoustine.lsp.Communicate -import langoustine.lsp.Invocation import langoustine.lsp.LSPBuilder import langoustine.lsp.app.LangoustineApp import langoustine.lsp.requests.LSPNotification From 10b0817aa17834206c9206d9a1a34c55b0a91d1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 14 May 2025 02:38:28 +0200 Subject: [PATCH 07/25] fixess --- modules/e2e/src/test/scala/playground/e2e/E2ETests.scala | 4 ++-- .../scala/playground/lsp2/LangoustineClientAdapter.scala | 6 +++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 666d7b4e7..82a22aa8d 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -50,7 +50,7 @@ object E2ETests extends SimpleIOSuite { } - private def runServer2: Resource[IO, Communicate[IO]] = Processes[IO] + private def runServer: Resource[IO, Communicate[IO]] = Processes[IO] .spawn(fs2.io.process.ProcessBuilder("cs", "launch", BuildInfo.lspArtifact)) .flatMap { process => val clientEndpoints: LSPBuilder[IO] => LSPBuilder[IO] = @@ -125,7 +125,7 @@ object E2ETests extends SimpleIOSuite { ) test("server startup and initialize") { - runServer2 + runServer .use { ls => file.Files[IO].tempDirectory.use { tempDirectory => val initParams = initializeParams(workspaceFolders = List(tempDirectory)) diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala index 38f1b221f..830648c01 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineClientAdapter.scala @@ -37,7 +37,11 @@ object LangoustineClientAdapter { ) .flatMap(_.headOption.liftTo[F](new Throwable("missing entry in the response"))) .map(converters.fromLSP.json) - .flatMap(_.as[A](v.codec).liftTo[F]) + .flatMap( + _.as[A]( + using v.codec + ).liftTo[F] + ) def refreshCodeLenses: F[Unit] = comms.request(workspace.codeLens.refresh(())).void def refreshDiagnostics: F[Unit] = comms.request(workspace.diagnostic.refresh(())).void From a58745d19c5074311632169c0168a8e07b35cd9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Wed, 14 May 2025 22:12:47 +0200 Subject: [PATCH 08/25] 0.0.24 --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index fdcfe3931..b702563fe 100644 --- a/build.sbt +++ b/build.sbt @@ -235,7 +235,7 @@ lazy val lsp = module("lsp") lazy val lsp2 = module("lsp2") .settings( libraryDependencies ++= Seq( - "tech.neander" %% "langoustine-app" % "0.0.23+3-8cfea919-SNAPSHOT" + "tech.neander" %% "langoustine-app" % "0.0.24" ).pipe(jsoniterFix) ) .dependsOn(lspKernel) From 91b5fa4e08c4a4480cf727b526a1e9ff58931de3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Fri, 16 May 2025 02:26:46 +0200 Subject: [PATCH 09/25] skip log --- .../main/scala/playground/lsp2/LangoustineServerAdapter.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala index b44cddd5e..7b20b3435 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/LangoustineServerAdapter.scala @@ -53,7 +53,6 @@ object LangoustineServerAdapter { def adapt[F[_]: ApplicativeThrow](server: playground.lsp.LanguageServer[F]) : LSPBuilder[F] => LSPBuilder[F] = _.handleRequest(initialize) { req => - System.err.println("got initialize request") server .initialize( req.params.workspaceFolders.toOption.foldMap(_.toOption.orEmpty).toList.map { From c578d815c93c2b22a08d881043bc9155fcf08393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Mon, 9 Jun 2025 02:10:23 +0200 Subject: [PATCH 10/25] bump --- build.sbt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 199f424c5..bb3c491c5 100644 --- a/build.sbt +++ b/build.sbt @@ -235,7 +235,7 @@ lazy val lsp = module("lsp") lazy val lsp2 = module("lsp2") .settings( libraryDependencies ++= Seq( - "tech.neander" %% "langoustine-app" % "0.0.24" + "tech.neander" %% "langoustine-app" % "0.0.25" ).pipe(jsoniterFix) ) .dependsOn(lspKernel) From 7661ccdc0c410a7503fc954710f37269d1e02d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Mon, 9 Jun 2025 03:34:40 +0200 Subject: [PATCH 11/25] Add .name --- modules/e2e/src/test/scala/playground/e2e/E2ETests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 3fb66d7a6..83e6c1494 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -57,7 +57,7 @@ object E2ETests extends SimpleIOSuite { _.handleNotification(window.showMessage) { in => val messageParams = in.params IO.println { - s"${Console.MAGENTA}Message from server: ${messageParams.message} (type: ${messageParams.`type`})${Console.RESET}" + s"${Console.MAGENTA}Message from server: ${messageParams.message} (type: ${messageParams.`type`.name})${Console.RESET}" } } From 5382b3c131706693e964ee6b101df552ea34eb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:10:36 +0200 Subject: [PATCH 12/25] add some logz --- modules/e2e/src/test/scala/playground/e2e/E2ETests.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 83e6c1494..504712f60 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -136,8 +136,8 @@ object E2ETests extends SimpleIOSuite { "Smithy Playground", ) } - } + } <* IO.println("Finished inner test") } - .timeout(20.seconds) + .timeout(20.seconds) <* IO.println("Finished test and closed server") } } From b2207d7f0c1b9d0f1c9f3ff3ca318c893ee5c096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:28:09 +0200 Subject: [PATCH 13/25] more verbose --- .../src/test/scala/playground/e2e/E2ETests.scala | 14 ++++++++++---- .../lsp/src/main/scala/playground/lsp/Main.scala | 7 +++++-- .../lsp2/src/main/scala/playground/lsp2/Main.scala | 5 +++++ 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 504712f60..3ba70c494 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -52,6 +52,7 @@ object E2ETests extends SimpleIOSuite { private def runServer: Resource[IO, Communicate[IO]] = Processes[IO] .spawn(fs2.io.process.ProcessBuilder("cs", "launch", BuildInfo.lspArtifact)) + .onFinalize(IO.println("Server process finalized")) .flatMap { process => val clientEndpoints: LSPBuilder[IO] => LSPBuilder[IO] = _.handleNotification(window.showMessage) { in => @@ -61,10 +62,9 @@ object E2ETests extends SimpleIOSuite { } } - FS2Channel[IO]() - .compile - .resource - .onlyOrError + FS2Channel + .resource[IO]() + .onFinalize(IO.println("Channel finalized")) .flatMap { chan => val comms = Communicate.channel(chan) chan @@ -89,10 +89,16 @@ object E2ETests extends SimpleIOSuite { .through(process.stdin) ) .concurrently(process.stderr.through(fs2.io.stderr[IO])) + .onFinalize( + IO.println("stdio streams finalized") + ) .compile .drain .background .as(comms) + .onFinalize( + IO.println("Server process and channel finalized") + ) } } } diff --git a/modules/lsp/src/main/scala/playground/lsp/Main.scala b/modules/lsp/src/main/scala/playground/lsp/Main.scala index 86f38e1d4..117401084 100644 --- a/modules/lsp/src/main/scala/playground/lsp/Main.scala +++ b/modules/lsp/src/main/scala/playground/lsp/Main.scala @@ -28,8 +28,11 @@ object Main extends IOApp.Simple { launch(stdin_raw, stdout_raw) ) .use { launcher => - IO.interruptibleMany(launcher.startListening().get()) - } *> IO.println("Server terminated without errors") + IO.interruptibleMany(launcher.startListening().get()).void + } + .guaranteeCase { oc => + IO.println(s"Server terminated with result: $oc") + } def launch( in: InputStream, diff --git a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala index bbc1b1d84..e1702fb4a 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala @@ -1,5 +1,6 @@ package playground.lsp2 +import cats.effect.ExitCode import cats.effect.IO import cats.effect.kernel.Deferred import cats.effect.kernel.Resource @@ -14,6 +15,10 @@ import langoustine.lsp.requests.LSPRequest object Main extends LangoustineApp { + override def run(args: List[String]): IO[ExitCode] = super + .run(args) + .guaranteeCase(ec => IO.println(s"Server terminated with result: $ec")) + def server(args: List[String]): Resource[IO, LSPBuilder[IO]] = IO .deferred[playground.lsp.LanguageClient[IO]] .toResource From a98f905f498773340f4bd549a4d5e58be03eb34a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:29:54 +0200 Subject: [PATCH 14/25] restore stream --- modules/e2e/src/test/scala/playground/e2e/E2ETests.scala | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 3ba70c494..744fb1282 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -62,8 +62,10 @@ object E2ETests extends SimpleIOSuite { } } - FS2Channel - .resource[IO]() + FS2Channel[IO]() + .compile + .resource + .onlyOrError .onFinalize(IO.println("Channel finalized")) .flatMap { chan => val comms = Communicate.channel(chan) From ba2413e504414b167e67c28b821293c1028ab1b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:33:14 +0200 Subject: [PATCH 15/25] i'm going insane, anyone want anything? --- build.sbt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index cedc16a1a..bd8db8c22 100644 --- a/build.sbt +++ b/build.sbt @@ -267,8 +267,11 @@ lazy val e2e = module("e2e") ), publish / skip := true, Test / fork := true, + libraryDependencies ++= Seq( + "tech.neander" %% "langoustine-lsp" % "0.0.25" + ).pipe(jsoniterFix), ) - .dependsOn(lsp, lsp2) + .dependsOn(lspKernel) val writeVersion = taskKey[Unit]("Writes the current version to the `.version` file") From 5cf1ff934ecbedc202b65474dcc4eab4d06a902d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:35:17 +0200 Subject: [PATCH 16/25] cleanup, we cool now --- build.sbt | 3 ++- .../test/scala/playground/e2e/E2ETests.scala | 22 ------------------- 2 files changed, 2 insertions(+), 23 deletions(-) diff --git a/build.sbt b/build.sbt index bd8db8c22..07d6ca08a 100644 --- a/build.sbt +++ b/build.sbt @@ -268,7 +268,8 @@ lazy val e2e = module("e2e") publish / skip := true, Test / fork := true, libraryDependencies ++= Seq( - "tech.neander" %% "langoustine-lsp" % "0.0.25" + "tech.neander" %% "langoustine-lsp" % "0.0.25", + "tech.neander" %% "jsonrpclib-fs2" % "0.0.7", ).pipe(jsoniterFix), ) .dependsOn(lspKernel) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 744fb1282..f1b8c6aa5 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -16,18 +16,6 @@ import langoustine.lsp.requests.initialize import langoustine.lsp.requests.window import langoustine.lsp.runtime.Opt import langoustine.lsp.runtime.Uri -import org.eclipse.lsp4j.ClientCapabilities -import org.eclipse.lsp4j.InitializeParams -import org.eclipse.lsp4j.InitializeResult -import org.eclipse.lsp4j.MessageActionItem -import org.eclipse.lsp4j.MessageParams -import org.eclipse.lsp4j.PublishDiagnosticsParams -import org.eclipse.lsp4j.ShowMessageRequestParams -import org.eclipse.lsp4j.WorkspaceFolder -import org.eclipse.lsp4j.launch.LSPLauncher -import org.eclipse.lsp4j.services.LanguageServer -import playground.e2e.E2ETests.LanguageServerAdapter -import playground.lsp.PlaygroundLanguageClient import weaver.* import java.io.PrintWriter @@ -40,16 +28,6 @@ import scala.util.chaining.* object E2ETests extends SimpleIOSuite { - class LanguageServerAdapter( - val ls: LanguageServer - ) { - - def initialize( - params: InitializeParams - ): IO[InitializeResult] = IO.fromCompletableFuture(IO(ls.initialize(params))) - - } - private def runServer: Resource[IO, Communicate[IO]] = Processes[IO] .spawn(fs2.io.process.ProcessBuilder("cs", "launch", BuildInfo.lspArtifact)) .onFinalize(IO.println("Server process finalized")) From 7052536b3f907abebca53afc46a186a06a589141 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:47:29 +0200 Subject: [PATCH 17/25] add onFinalizes --- .../e2e/src/test/scala/playground/e2e/E2ETests.scala | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index f1b8c6aa5..56ed91926 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -60,6 +60,7 @@ object E2ETests extends SimpleIOSuite { .observe(_.through(fs2.text.utf8.decode[IO]).debug("stdout: " + _).drain) .through(jsonrpclib.fs2.lsp.decodeMessages[IO]) .through(channel.inputOrBounce) + .onFinalize(IO.println("stdout stream finalized")) ) .concurrently( channel @@ -67,8 +68,16 @@ object E2ETests extends SimpleIOSuite { .through(jsonrpclib.fs2.lsp.encodeMessages[IO]) .observe(_.through(fs2.text.utf8.decode[IO]).debug("stdin: " + _).drain) .through(process.stdin) + .onFinalize(IO.println("stdin stream finalized")) + ) + .concurrently( + process + .stderr + .through(fs2.io.stderr[IO]) + .onFinalize( + IO.println("stderr stream finalized") + ) ) - .concurrently(process.stderr.through(fs2.io.stderr[IO])) .onFinalize( IO.println("stdio streams finalized") ) From bff313ba082fe6ad0d124bca40f8b9be6f45555a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:47:53 +0200 Subject: [PATCH 18/25] simplify for test --- build.sbt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index 07d6ca08a..053fbd9d1 100644 --- a/build.sbt +++ b/build.sbt @@ -281,7 +281,8 @@ lazy val root = project .settings( publish / skip := true, mimaFailOnNoPrevious := false, - addCommandAlias("ci", "+test;+mimaReportBinaryIssues;+publishLocal;writeVersion"), + addCommandAlias("ci", "e2e/test"), + // addCommandAlias("ci", "+test;+mimaReportBinaryIssues;+publishLocal;writeVersion"), writeVersion := { IO.write(file(".version"), version.value) }, From 125c5b78d6d26ab507724682cfdd12ace804e6f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:57:10 +0200 Subject: [PATCH 19/25] small change --- modules/e2e/src/test/scala/playground/e2e/E2ETests.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 56ed91926..2a1c28a25 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -52,7 +52,7 @@ object E2ETests extends SimpleIOSuite { .flatMap { channel => fs2 .Stream - .never[IO] + .eval(IO.never) .concurrently( process .stdout From 5edf6aa623521f4353b3faf4b8788d8cb35bdaa6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 02:57:49 +0200 Subject: [PATCH 20/25] println --- modules/e2e/src/test/scala/playground/e2e/E2ETests.scala | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 2a1c28a25..1a313c83f 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -83,6 +83,9 @@ object E2ETests extends SimpleIOSuite { ) .compile .drain + .guarantee( + IO.println("Finalizing server process fiber") + ) .background .as(comms) .onFinalize( From 66d322f4867666d838998fc0298fbe35dbdd6cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sat, 14 Jun 2025 03:08:49 +0200 Subject: [PATCH 21/25] eh --- .../test/scala/playground/e2e/E2ETests.scala | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index 1a313c83f..d818d3aa3 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -50,18 +50,13 @@ object E2ETests extends SimpleIOSuite { chan .withEndpoints(clientEndpoints(LSPBuilder.create[IO]).build(comms)) .flatMap { channel => - fs2 - .Stream - .eval(IO.never) - .concurrently( - process - .stdout - // fs2.io.stdout seems to be printed repeatedly for some reason, could be a bug with sbt - .observe(_.through(fs2.text.utf8.decode[IO]).debug("stdout: " + _).drain) - .through(jsonrpclib.fs2.lsp.decodeMessages[IO]) - .through(channel.inputOrBounce) - .onFinalize(IO.println("stdout stream finalized")) - ) + process + .stdout + // fs2.io.stdout seems to be printed repeatedly for some reason, could be a bug with sbt + .observe(_.through(fs2.text.utf8.decode[IO]).debug("stdout: " + _).drain) + .through(jsonrpclib.fs2.lsp.decodeMessages[IO]) + .through(channel.inputOrBounce) + .onFinalize(IO.println("stdout stream finalized")) .concurrently( channel .output From 0e7bfcd1b1668f16aa663eacf9aee03f41c17825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Sun, 10 Aug 2025 23:57:58 +0200 Subject: [PATCH 22/25] improve server-side logging --- modules/lsp2/src/main/scala/playground/lsp2/Main.scala | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala index e1702fb4a..91c0cd96e 100644 --- a/modules/lsp2/src/main/scala/playground/lsp2/Main.scala +++ b/modules/lsp2/src/main/scala/playground/lsp2/Main.scala @@ -17,7 +17,7 @@ object Main extends LangoustineApp { override def run(args: List[String]): IO[ExitCode] = super .run(args) - .guaranteeCase(ec => IO.println(s"Server terminated with result: $ec")) + .guaranteeCase(ec => IO.consoleForIO.errorln(s"Server terminated with result: $ec")) def server(args: List[String]): Resource[IO, LSPBuilder[IO]] = IO .deferred[playground.lsp.LanguageClient[IO]] @@ -33,6 +33,7 @@ object Main extends LangoustineApp { .map(LangoustineServerAdapter.adapt(_).apply(LSPBuilder.create[IO])) .map(bindClient(_, clientRef)) } + .onFinalizeCase(ec => IO.consoleForIO.errorln("exiting server: " + ec)) private def bindClient( lsp: LSPBuilder[IO], From 6ce835dc37a972fff07c9f795c1dbefe5cffee0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Mon, 11 Aug 2025 00:04:41 +0200 Subject: [PATCH 23/25] Dump traces during e2e test --- .../test/scala/playground/e2e/E2ETests.scala | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index d818d3aa3..c6195e534 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -118,19 +118,31 @@ object E2ETests extends SimpleIOSuite { ) test("server startup and initialize") { - runServer - .use { ls => - file.Files[IO].tempDirectory.use { tempDirectory => - val initParams = initializeParams(workspaceFolders = List(tempDirectory)) + val impl = + runServer + .use { ls => + file.Files[IO].tempDirectory.use { tempDirectory => + val initParams = initializeParams(workspaceFolders = List(tempDirectory)) - ls.request(initialize(initParams)).map { result => - expect.eql( - result.serverInfo.toOption.get.name, - "Smithy Playground", - ) + ls.request(initialize(initParams)).map { result => + expect.eql( + result.serverInfo.toOption.get.name, + "Smithy Playground", + ) + } + } <* IO.println("Finished inner test") + } + .timeout(20.seconds) <* IO.println("Finished test and closed server") + + impl + .race( + IO + .trace + .flatMap { trace => + IO.println(trace.pretty).andWait(5.seconds) } - } <* IO.println("Finished inner test") - } - .timeout(20.seconds) <* IO.println("Finished test and closed server") + .foreverM + ) + .map(_.merge) } } From a5efe91617356f99944e6960c6fc9686f0b451f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Mon, 11 Aug 2025 00:11:20 +0200 Subject: [PATCH 24/25] Revert "Dump traces during e2e test" This reverts commit 6ce835dc37a972fff07c9f795c1dbefe5cffee0a. --- .../test/scala/playground/e2e/E2ETests.scala | 36 +++++++------------ 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index c6195e534..d818d3aa3 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -118,31 +118,19 @@ object E2ETests extends SimpleIOSuite { ) test("server startup and initialize") { - val impl = - runServer - .use { ls => - file.Files[IO].tempDirectory.use { tempDirectory => - val initParams = initializeParams(workspaceFolders = List(tempDirectory)) + runServer + .use { ls => + file.Files[IO].tempDirectory.use { tempDirectory => + val initParams = initializeParams(workspaceFolders = List(tempDirectory)) - ls.request(initialize(initParams)).map { result => - expect.eql( - result.serverInfo.toOption.get.name, - "Smithy Playground", - ) - } - } <* IO.println("Finished inner test") - } - .timeout(20.seconds) <* IO.println("Finished test and closed server") - - impl - .race( - IO - .trace - .flatMap { trace => - IO.println(trace.pretty).andWait(5.seconds) + ls.request(initialize(initParams)).map { result => + expect.eql( + result.serverInfo.toOption.get.name, + "Smithy Playground", + ) } - .foreverM - ) - .map(_.merge) + } <* IO.println("Finished inner test") + } + .timeout(20.seconds) <* IO.println("Finished test and closed server") } } From f48261ca5e94456f7e94349f9b870eddfc127a1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Koz=C5=82owski?= Date: Mon, 11 Aug 2025 00:19:13 +0200 Subject: [PATCH 25/25] do a full fiber dump --- .../test/scala/playground/e2e/E2ETests.scala | 38 ++++++++++++------- .../test/scala/playground/e2e/IOHack.scala | 10 +++++ 2 files changed, 35 insertions(+), 13 deletions(-) create mode 100644 modules/e2e/src/test/scala/playground/e2e/IOHack.scala diff --git a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala index d818d3aa3..5fa5cc89b 100644 --- a/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala +++ b/modules/e2e/src/test/scala/playground/e2e/E2ETests.scala @@ -3,7 +3,9 @@ package playground.e2e import buildinfo.BuildInfo import cats.effect.Concurrent import cats.effect.IO +import cats.effect.IOHack import cats.effect.kernel.Resource +import cats.effect.unsafe.IORuntime import cats.effect.unsafe.implicits.* import cats.syntax.all.* import fs2.io.file @@ -118,19 +120,29 @@ object E2ETests extends SimpleIOSuite { ) test("server startup and initialize") { - runServer - .use { ls => - file.Files[IO].tempDirectory.use { tempDirectory => - val initParams = initializeParams(workspaceFolders = List(tempDirectory)) + val run = + runServer + .use { ls => + file.Files[IO].tempDirectory.use { tempDirectory => + val initParams = initializeParams(workspaceFolders = List(tempDirectory)) - ls.request(initialize(initParams)).map { result => - expect.eql( - result.serverInfo.toOption.get.name, - "Smithy Playground", - ) - } - } <* IO.println("Finished inner test") - } - .timeout(20.seconds) <* IO.println("Finished test and closed server") + ls.request(initialize(initParams)).map { result => + expect.eql( + result.serverInfo.toOption.get.name, + "Smithy Playground", + ) + } + } <* IO.println("Finished inner test") + } + .timeout(20.seconds) <* IO.println("Finished test and closed server") + + run + .race( + IO(triggerFiberSnapshot()).andWait(5.seconds).foreverM + ) + .map(_.merge) } + + def triggerFiberSnapshot(): Unit = IOHack.fiberSnapshot() + } diff --git a/modules/e2e/src/test/scala/playground/e2e/IOHack.scala b/modules/e2e/src/test/scala/playground/e2e/IOHack.scala new file mode 100644 index 000000000..d93800ce7 --- /dev/null +++ b/modules/e2e/src/test/scala/playground/e2e/IOHack.scala @@ -0,0 +1,10 @@ +package cats.effect + +object IOHack { + + def fiberSnapshot() = { + val runtime = cats.effect.unsafe.implicits.global + runtime.fiberMonitor.liveFiberSnapshot(System.err.print(_)) + } + +}