From 2863498da34328484a3d0bf7ad3a4604f3c24c30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Olczak?= Date: Wed, 18 Mar 2020 11:41:55 +0100 Subject: [PATCH] Scaffold the Project Manager (#610) --- build.sbt | 39 +++- .../jsonrpc/test/JsonRpcServerTestKit.scala | 58 ++---- .../jsonrpc/ClientControllerFactory.scala | 22 +++ .../org/enso}/jsonrpc/JsonProtocol.scala | 3 +- .../org/enso/jsonrpc/JsonRpcServer.scala | 107 ++++++----- .../org/enso}/jsonrpc/MessageHandler.scala | 4 +- .../scala/org/enso}/jsonrpc/Protocol.scala | 2 +- .../org/enso/jsonrpc/MessageHandlerSpec.scala | 21 +-- .../src/main/resources/application.conf | 4 +- .../org/enso/projectmanager/RouteHelper.scala | 30 ---- .../org/enso/projectmanager/Server.scala | 168 ------------------ .../org/enso/projectmanager/api/Project.scala | 13 +- .../enso/projectmanager/main/MainModule.scala | 27 +++ .../projectmanager/main/ProjectManager.scala | 43 +++++ .../projectmanager/main/configuration.scala | 20 +++ .../protocol/ClientController.scala | 27 +++ .../projectmanager/protocol/JsonRpc.scala | 16 ++ .../ManagerClientControllerFactory.scala | 25 +++ .../protocol/ProjectManagementApi.scala | 24 +++ .../org/enso/languageserver/MainModule.scala | 23 +-- .../capability/CapabilityApi.scala | 2 +- .../filemanager/FileManagerApi.scala | 8 +- .../filemanager/FileSystemFailureMapper.scala | 2 +- .../{ => protocol}/ClientController.scala | 93 +++++----- .../languageserver/protocol/JsonRpc.scala | 38 ++++ .../ServerClientControllerFactory.scala | 33 ++++ .../AcquireCapabilityHandler.scala | 4 +- .../requesthandler/ApplyEditHandler.scala | 20 +-- .../requesthandler/CloseFileHandler.scala | 4 +- .../requesthandler/OpenFileHandler.scala | 9 +- .../ReleaseCapabilityHandler.scala | 4 +- .../requesthandler/SaveFileHandler.scala | 12 +- .../enso/languageserver/text/TextApi.scala | 8 +- .../websocket/BaseServerTest.scala | 48 +++++ .../websocket/FileManagerTest.scala | 2 +- .../websocket/TextOperationsTest.scala | 2 +- .../org/enso/runner/LanguageServerApp.scala | 14 +- 37 files changed, 522 insertions(+), 457 deletions(-) rename engine/language-server/src/test/scala/org/enso/languageserver/websocket/WebSocketServerTest.scala => common/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala (62%) create mode 100644 common/json-rpc-server/src/main/scala/org/enso/jsonrpc/ClientControllerFactory.scala rename {engine/language-server/src/main/scala/org/enso/languageserver => common/json-rpc-server/src/main/scala/org/enso}/jsonrpc/JsonProtocol.scala (99%) rename engine/language-server/src/main/scala/org/enso/languageserver/WebSocketServer.scala => common/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala (69%) rename {engine/language-server/src/main/scala/org/enso/languageserver => common/json-rpc-server/src/main/scala/org/enso}/jsonrpc/MessageHandler.scala (98%) rename {engine/language-server/src/main/scala/org/enso/languageserver => common/json-rpc-server/src/main/scala/org/enso}/jsonrpc/Protocol.scala (99%) rename engine/language-server/src/test/scala/org/enso/languageserver/MessageHandlerTest.scala => common/json-rpc-server/src/test/scala/org/enso/jsonrpc/MessageHandlerSpec.scala (96%) delete mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/RouteHelper.scala delete mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/Server.scala create mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/main/MainModule.scala create mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/main/ProjectManager.scala create mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/main/configuration.scala create mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ClientController.scala create mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala create mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ManagerClientControllerFactory.scala create mode 100644 common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala rename engine/language-server/src/main/scala/org/enso/languageserver/{ => protocol}/ClientController.scala (86%) create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/protocol/JsonRpc.scala create mode 100644 engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala create mode 100644 engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala diff --git a/build.sbt b/build.sbt index 39ee6bd968..9f906f2a44 100644 --- a/build.sbt +++ b/build.sbt @@ -347,9 +347,19 @@ lazy val project_manager = (project in file("common/project-manager")) ) .settings( libraryDependencies ++= akka, - libraryDependencies ++= circe + libraryDependencies ++= circe, + libraryDependencies ++= Seq( + // config + "com.typesafe" % "config" % "1.4.0", + "com.github.pureconfig" %% "pureconfig" % "0.12.2", + // logging + "ch.qos.logback" % "logback-classic" % "1.2.3", + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.2" + ) ) .dependsOn(pkg) + .dependsOn(`json-rpc-server`) + .dependsOn(`json-rpc-server-test` % Test) ////////////////////// //// Sub Projects //// @@ -453,6 +463,8 @@ lazy val language_server = (project in file("engine/language-server")) ) ) .dependsOn(polyglot_api) + .dependsOn(`json-rpc-server`) + .dependsOn(`json-rpc-server-test` % Test) lazy val runtime = (project in file("engine/runtime")) .configs(Benchmark) @@ -611,3 +623,28 @@ lazy val runner = project .dependsOn(pkg) .dependsOn(language_server) .dependsOn(polyglot_api) + +lazy val `json-rpc-server` = project + .in(file("common/json-rpc-server")) + .settings( + libraryDependencies ++= akka, + libraryDependencies ++= circe, + libraryDependencies ++= Seq( + "io.circe" %% "circe-literal" % circeVersion, + akkaTestkit % Test, + "org.scalatest" %% "scalatest" % "3.2.0-M2" % Test + ) + ) + +lazy val `json-rpc-server-test` = project + .in(file("common/json-rpc-server-test")) + .settings( + libraryDependencies ++= akka, + libraryDependencies ++= circe, + libraryDependencies ++= Seq( + "io.circe" %% "circe-literal" % circeVersion, + akkaTestkit, + "org.scalatest" %% "scalatest" % "3.2.0-M2" + ) + ) + .dependsOn(`json-rpc-server`) diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/WebSocketServerTest.scala b/common/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala similarity index 62% rename from engine/language-server/src/test/scala/org/enso/languageserver/websocket/WebSocketServerTest.scala rename to common/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala index b131333076..6b9ec62c32 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/WebSocketServerTest.scala +++ b/common/json-rpc-server-test/src/main/scala/org/enso/jsonrpc/test/JsonRpcServerTestKit.scala @@ -1,35 +1,26 @@ -package org.enso.languageserver.websocket -import java.nio.file.Files -import java.util.UUID +package org.enso.jsonrpc.test import akka.NotUsed -import akka.actor.{ActorRef, ActorSystem, PoisonPill, Props} +import akka.actor.{ActorRef, ActorSystem, PoisonPill} import akka.http.scaladsl.Http import akka.http.scaladsl.model.ws.{Message, TextMessage, WebSocketRequest} import akka.stream.OverflowStrategy import akka.stream.scaladsl.{Flow, Sink, Source} import akka.testkit.{ImplicitSender, TestKit, TestProbe} -import cats.effect.IO import io.circe.Json import io.circe.parser.parse -import org.enso.languageserver.capability.CapabilityRouter -import org.enso.languageserver.data.{Config, Sha3_224VersionCalculator} -import org.enso.languageserver.{ - LanguageProtocol, - LanguageServer, - WebSocketServer -} -import org.enso.languageserver.filemanager.FileSystem -import org.enso.languageserver.runtime.RuntimeConnector -import org.enso.languageserver.text.BufferRegistry -import org.scalatest.{Assertion, BeforeAndAfterAll, BeforeAndAfterEach} +import org.enso.jsonrpc.{ClientControllerFactory, JsonRpcServer, Protocol} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike +import org.scalatest.{Assertion, BeforeAndAfterAll, BeforeAndAfterEach} import scala.concurrent.Await import scala.concurrent.duration._ -abstract class WebSocketServerTest +/** + * Test kit for testing JSON RPC servers. + */ +abstract class JsonRpcServerTestKit extends TestKit(ActorSystem("TestSystem")) with ImplicitSender with AnyWordSpecLike @@ -44,37 +35,16 @@ abstract class WebSocketServerTest val interface = "127.0.0.1" var address: String = _ - val testContentRoot = Files.createTempDirectory(null) - val testContentRootId = UUID.randomUUID() - val config = Config(Map(testContentRootId -> testContentRoot.toFile)) - - testContentRoot.toFile.deleteOnExit() - - var server: WebSocketServer = _ + var server: JsonRpcServer = _ var binding: Http.ServerBinding = _ + def protocol: Protocol + + def clientControllerFactory: ClientControllerFactory + override def beforeEach(): Unit = { - val languageServer = - system.actorOf( - Props(new LanguageServer(config, new FileSystem[IO])) - ) - languageServer ! LanguageProtocol.Initialize - val bufferRegistry = - system.actorOf( - BufferRegistry.props(languageServer)(Sha3_224VersionCalculator) - ) - lazy val capabilityRouter = - system.actorOf(CapabilityRouter.props(bufferRegistry)) - - lazy val runtimeConnector = system.actorOf(RuntimeConnector.props) - - server = new WebSocketServer( - languageServer, - bufferRegistry, - capabilityRouter, - runtimeConnector - ) + server = new JsonRpcServer(protocol, clientControllerFactory) binding = Await.result(server.bind(interface, port = 0), 3.seconds) address = s"ws://$interface:${binding.localAddress.getPort}" } diff --git a/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/ClientControllerFactory.scala b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/ClientControllerFactory.scala new file mode 100644 index 0000000000..90637fe768 --- /dev/null +++ b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/ClientControllerFactory.scala @@ -0,0 +1,22 @@ +package org.enso.jsonrpc + +import java.util.UUID + +import akka.actor.ActorRef + +/** + * Classes implementing this trait are responsible for creating client + * controllers upon a new connection. An client controller handles + * communications between a single client and the JSON RPC server. + */ +trait ClientControllerFactory { + + /** + * Creates a client controller actor. + * + * @param clientId the internal client id. + * @return an actor ref to the client controller + */ + def createClientController(clientId: UUID): ActorRef + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/JsonProtocol.scala b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonProtocol.scala similarity index 99% rename from engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/JsonProtocol.scala rename to common/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonProtocol.scala index a380fb0a95..9aa28f3022 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/JsonProtocol.scala +++ b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonProtocol.scala @@ -1,4 +1,5 @@ -package org.enso.languageserver.jsonrpc +package org.enso.jsonrpc + import io.circe.Decoder.Result import io.circe._ diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/WebSocketServer.scala b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala similarity index 69% rename from engine/language-server/src/main/scala/org/enso/languageserver/WebSocketServer.scala rename to common/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala index 5414fb1796..fec0315f4e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/WebSocketServer.scala +++ b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/JsonRpcServer.scala @@ -1,4 +1,4 @@ -package org.enso.languageserver +package org.enso.jsonrpc import java.util.UUID @@ -6,50 +6,27 @@ import akka.NotUsed import akka.actor.{ActorRef, ActorSystem, Props} import akka.http.scaladsl.Http import akka.http.scaladsl.model.ws.{BinaryMessage, Message, TextMessage} -import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server.Directives.{get, handleWebSocketMessages, path} import akka.http.scaladsl.server.Route -import akka.stream.scaladsl.{Flow, Sink, Source} import akka.stream.{Materializer, OverflowStrategy} -import org.enso.languageserver.jsonrpc.MessageHandler +import akka.stream.scaladsl.{Flow, Sink, Source} -import scala.concurrent.duration.{FiniteDuration, _} import scala.concurrent.{ExecutionContext, Future} - -object WebSocketServer { - - /** - * A configuration object for properties of the WebSocketServer. - * - * @param outgoingBufferSize the number of messages buffered internally - * if the downstream connection is lagging behind. - * @param lazyMessageTimeout the timeout for downloading the whole of a lazy - * stream message from the user. - */ - case class Config(outgoingBufferSize: Int, lazyMessageTimeout: FiniteDuration) - - case object Config { - - /** - * Creates a default instance of [[Config]]. - * - * @return a default config. - */ - def default: Config = - Config(outgoingBufferSize = 10, lazyMessageTimeout = 10.seconds) - } -} +import scala.concurrent.duration._ /** - * Exposes a multi-client Lanugage Server instance over WebSocket connections. - * @param languageServer an instance of a running and initialized Language - * Server. + * Exposes a multi-client JSON RPC Server instance over WebSocket connections. + * + * @param protocol a protocol supported be the server + * @param clientControllerFactory a factory used to create a client controller + * @param config a server config + * @param system an actor system + * @param materializer a materializer */ -class WebSocketServer( - languageServer: ActorRef, - bufferRegistry: ActorRef, - capabilityRouter: ActorRef, - runtimeConnector: ActorRef, - config: WebSocketServer.Config = WebSocketServer.Config.default +class JsonRpcServer( + protocol: Protocol, + clientControllerFactory: ClientControllerFactory, + config: JsonRpcServer.Config = JsonRpcServer.Config.default )( implicit val system: ActorSystem, implicit val materializer: Materializer @@ -57,27 +34,15 @@ class WebSocketServer( implicit val ec: ExecutionContext = system.dispatcher - private val newConnectionPath: String = "" - private def newUser(): Flow[Message, Message, NotUsed] = { - val clientId = UUID.randomUUID() - val clientActor = - system.actorOf( - Props( - new ClientController( - clientId, - languageServer, - bufferRegistry, - capabilityRouter - ) - ) - ) + val clientId = UUID.randomUUID() + val clientActor = clientControllerFactory.createClientController(clientId) val messageHandler = system.actorOf( - Props(new MessageHandler(ClientApi.protocol, clientActor)) + Props(new MessageHandler(protocol, clientActor)) ) - clientActor ! ClientApi.WebConnect(messageHandler) + clientActor ! JsonRpcServer.WebConnect(messageHandler) val incomingMessages: Sink[Message, NotUsed] = Flow[Message] @@ -117,7 +82,7 @@ class WebSocketServer( Flow.fromSinkAndSource(incomingMessages, outgoingMessages) } - private val route: Route = path(newConnectionPath) { + private val route: Route = path(config.path) { get { handleWebSocketMessages(newUser()) } } @@ -132,3 +97,35 @@ class WebSocketServer( def bind(interface: String, port: Int): Future[Http.ServerBinding] = Http().bindAndHandle(route, interface, port) } + +object JsonRpcServer { + + /** + * A configuration object for properties of the JsonRpcServer. + * + * @param outgoingBufferSize the number of messages buffered internally + * if the downstream connection is lagging behind. + * @param lazyMessageTimeout the timeout for downloading the whole of a lazy + * stream message from the user. + * @param path the http path that the server listen to. + */ + case class Config( + outgoingBufferSize: Int, + lazyMessageTimeout: FiniteDuration, + path: String = "" + ) + + case object Config { + + /** + * Creates a default instance of [[Config]]. + * + * @return a default config. + */ + def default: Config = + Config(outgoingBufferSize = 10, lazyMessageTimeout = 10.seconds) + } + + case class WebConnect(webActor: ActorRef) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/MessageHandler.scala b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala similarity index 98% rename from engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/MessageHandler.scala rename to common/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala index b6f2e42912..6bcdabc99e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/MessageHandler.scala +++ b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/MessageHandler.scala @@ -1,8 +1,8 @@ -package org.enso.languageserver.jsonrpc +package org.enso.jsonrpc import akka.actor.{Actor, ActorRef, Stash} import io.circe.Json -import org.enso.languageserver.jsonrpc.Errors.InvalidParams +import org.enso.jsonrpc.Errors.InvalidParams /** * An actor responsible for passing parsed massages between the web and diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/Protocol.scala b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/Protocol.scala similarity index 99% rename from engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/Protocol.scala rename to common/json-rpc-server/src/main/scala/org/enso/jsonrpc/Protocol.scala index 21f680bf0d..65d72a02b5 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/jsonrpc/Protocol.scala +++ b/common/json-rpc-server/src/main/scala/org/enso/jsonrpc/Protocol.scala @@ -1,4 +1,4 @@ -package org.enso.languageserver.jsonrpc +package org.enso.jsonrpc import io.circe.{Decoder, Encoder, Json} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/MessageHandlerTest.scala b/common/json-rpc-server/src/test/scala/org/enso/jsonrpc/MessageHandlerSpec.scala similarity index 96% rename from engine/language-server/src/test/scala/org/enso/languageserver/MessageHandlerTest.scala rename to common/json-rpc-server/src/test/scala/org/enso/jsonrpc/MessageHandlerSpec.scala index d446b012a0..6a30f334e9 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/MessageHandlerTest.scala +++ b/common/json-rpc-server/src/test/scala/org/enso/jsonrpc/MessageHandlerSpec.scala @@ -1,31 +1,18 @@ -package org.enso.languageserver +package org.enso.jsonrpc + import akka.actor.{ActorRef, ActorSystem, Props} import akka.testkit.{ImplicitSender, TestKit, TestProbe} import io.circe.Json import io.circe.literal._ import io.circe.parser._ -import org.enso.languageserver.jsonrpc.MessageHandler.{Connected, WebMessage} -import org.enso.languageserver.jsonrpc.{ - Error, - HasParams, - HasResult, - Id, - MessageHandler, - Method, - Notification, - Protocol, - Request, - ResponseError, - ResponseResult, - Unused -} +import org.enso.jsonrpc.MessageHandler.{Connected, WebMessage} import org.scalatest.matchers.should.Matchers import org.scalatest.wordspec.AnyWordSpecLike import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach} import scala.concurrent.duration._ -class MessageHandlerTest +class MessageHandlerSpec extends TestKit(ActorSystem("TestSystem")) with ImplicitSender with AnyWordSpecLike diff --git a/common/project-manager/src/main/resources/application.conf b/common/project-manager/src/main/resources/application.conf index 3cde653792..d6593faed0 100644 --- a/common/project-manager/src/main/resources/application.conf +++ b/common/project-manager/src/main/resources/application.conf @@ -16,4 +16,6 @@ project-manager { tutorials { github-organisation = "luna-packages" } -} \ No newline at end of file +} + +akka.http.server.idle-timeout = infinite \ No newline at end of file diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/RouteHelper.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/RouteHelper.scala deleted file mode 100644 index de806e77fe..0000000000 --- a/common/project-manager/src/main/scala/org/enso/projectmanager/RouteHelper.scala +++ /dev/null @@ -1,30 +0,0 @@ -package org.enso.projectmanager - -import akka.http.scaladsl.model.Uri -import akka.http.scaladsl.model.Uri.Path -import akka.http.scaladsl.server.{PathMatcher0, PathMatcher1} -import akka.http.scaladsl.server.PathMatchers.JavaUUID -import org.enso.projectmanager.model.ProjectId - -class RouteHelper { - - val tutorials: String = "tutorials" - val projects: String = "projects" - val thumb: String = "thumb" - - val tutorialsPath: Path = Path / tutorials - val tutorialsPathMatcher: PathMatcher0 = tutorials - - val projectsPath: Path = Path / projects - val projectsPathMatcher: PathMatcher0 = projects - - def projectPath(id: ProjectId): Path = projectsPath / id.toString - - val projectPathMatcher: PathMatcher1[ProjectId] = - (projectsPathMatcher / JavaUUID).map(ProjectId) - - def thumbPath(id: ProjectId): Path = projectPath(id) / thumb - val thumbPathMatcher: PathMatcher1[ProjectId] = projectPathMatcher / thumb - - def uriFor(base: Uri, path: Path): Uri = base.withPath(path) -} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/Server.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/Server.scala deleted file mode 100644 index a5f3407f41..0000000000 --- a/common/project-manager/src/main/scala/org/enso/projectmanager/Server.scala +++ /dev/null @@ -1,168 +0,0 @@ -package org.enso.projectmanager - -import java.io.File -import java.util.concurrent.TimeUnit - -import akka.actor.ActorSystem -import akka.actor.typed.ActorRef -import akka.actor.typed.Scheduler -import akka.actor.typed.scaladsl.AskPattern._ -import akka.actor.typed.scaladsl.adapter._ -import akka.http.scaladsl.Http -import akka.http.scaladsl.model.{HttpResponse, StatusCodes, Uri} -import akka.http.scaladsl.server.{Directives, Route} -import akka.util.Timeout -import com.typesafe.config.ConfigFactory -import org.enso.projectmanager.api.{ProjectFactory, ProjectJsonSupport} -import org.enso.projectmanager.model.{Project, ProjectId} -import org.enso.projectmanager.services._ - -import scala.concurrent.{ExecutionContext, Future} -import scala.concurrent.duration._ -import scala.util.{Failure, Success} - -case class Server( - host: String, - port: Int, - repository: ActorRef[ProjectsCommand], - routeHelper: RouteHelper, - apiFactory: ProjectFactory -)(implicit val system: ActorSystem, - implicit val executor: ExecutionContext, - implicit val askTimeout: Timeout) - extends Directives - with ProjectJsonSupport { - - implicit val scheduler: Scheduler = system.scheduler.toTyped - - def projectDoesNotExistResponse(id: ProjectId): HttpResponse = - HttpResponse(StatusCodes.NotFound, entity = s"Project $id does not exist") - - def thumbDoesNotExistResponse: HttpResponse = - HttpResponse(StatusCodes.NotFound, entity = "Thumbnail does not exist") - - def withSuccess[T]( - fut: Future[T], - errorResponse: HttpResponse = HttpResponse(StatusCodes.InternalServerError) - )(successHandler: T => Route - ): Route = { - onComplete(fut) { - case Success(r) => successHandler(r) - case Failure(_) => complete(errorResponse) - } - } - - def withProject(id: ProjectId)(route: Project => Route): Route = { - val projectFuture = - repository - .ask( - (ref: ActorRef[GetProjectResponse]) => GetProjectById(id, ref) - ) - .map(_.project) - withSuccess(projectFuture) { - case Some(project) => route(project) - case None => complete(projectDoesNotExistResponse(id)) - } - } - - def listProjectsWith( - reqBuilder: ActorRef[ListProjectsResponse] => ProjectsCommand - )(baseUri: Uri - ): Route = { - val projectsFuture = repository.ask(reqBuilder) - withSuccess(projectsFuture) { projectsResponse => - val response = projectsResponse.projects.toSeq.map { - case (id, project) => apiFactory.fromModel(id, project, baseUri) - } - complete(response) - } - } - - def createProject(baseUri: Uri): Route = { - val projectFuture = repository.ask( - (ref: ActorRef[CreateTemporaryResponse]) => - CreateTemporary("NewProject", ref) - ) - withSuccess(projectFuture) { response => - complete(apiFactory.fromModel(response.id, response.project, baseUri)) - } - } - - def getThumb(projectId: ProjectId): Route = { - withProject(projectId) { project => - if (project.pkg.hasThumb) getFromFile(project.pkg.thumbFile) - else complete(thumbDoesNotExistResponse) - } - } - - val route: Route = ignoreTrailingSlash { - path(routeHelper.projectsPathMatcher)( - (get & extractUri)(listProjectsWith(ListProjectsRequest)) ~ - (post & extractUri)(createProject) - ) ~ - (get & path(routeHelper.tutorialsPathMatcher) & extractUri)( - listProjectsWith(ListTutorialsRequest) - ) ~ - (get & path(routeHelper.thumbPathMatcher))(getThumb) - } - - def serve: Future[Http.ServerBinding] = { - Http().bindAndHandle(route, host, port) - } -} - -object Server { - - def main(args: Array[String]): Unit = { - - val config = ConfigFactory.load.getConfig("project-manager") - val serverConfig = config.getConfig("server") - val storageConfig = config.getConfig("storage") - - val host = serverConfig.getString("host") - val port = serverConfig.getInt("port") - - val timeout = - FiniteDuration( - serverConfig.getDuration("timeout").toNanos, - TimeUnit.NANOSECONDS - ) - - implicit val system: ActorSystem = ActorSystem("project-manager") - implicit val executor: ExecutionContext = system.dispatcher - implicit val askTimeout: Timeout = new Timeout(timeout) - - val localProjectsPath = - new File(storageConfig.getString("local-projects-path")) - val tmpProjectsPath = new File( - storageConfig.getString("temporary-projects-path") - ) - val tutorialsPath = - new File(storageConfig.getString("tutorials-path")) - val tutorialsCachePath = - new File(storageConfig.getString("tutorials-cache-path")) - - val tutorialsDownloader = - TutorialsDownloader( - tutorialsPath, - tutorialsCachePath, - config.getString("tutorials.github-organisation") - ) - val storageManager = StorageManager( - localProjectsPath, - tmpProjectsPath, - tutorialsPath - ) - - val repoActor = system.spawn( - ProjectsService.behavior(storageManager, tutorialsDownloader), - "projects-repository" - ) - - val routeHelper = new RouteHelper - val apiFactory = ProjectFactory(routeHelper) - - val server = Server(host, port, repoActor, routeHelper, apiFactory) - server.serve: Unit - } -} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/api/Project.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/api/Project.scala index 7eec18400b..f218ae644f 100644 --- a/common/project-manager/src/main/scala/org/enso/projectmanager/api/Project.scala +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/api/Project.scala @@ -2,7 +2,7 @@ package org.enso.projectmanager.api import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import akka.http.scaladsl.model.Uri -import org.enso.projectmanager.{RouteHelper, model} +import org.enso.projectmanager.model import org.enso.projectmanager.model.ProjectId import spray.json.DefaultJsonProtocol @@ -11,24 +11,21 @@ case class Project( name: String, path: String, thumb: Option[String], - persisted: Boolean) + persisted: Boolean +) -case class ProjectFactory(routeHelper: RouteHelper) { +case class ProjectFactory() { def fromModel( id: ProjectId, project: model.Project, baseUri: Uri ): Project = { - val thumbUri = - if (project.hasThumb) - Some(routeHelper.uriFor(baseUri, routeHelper.thumbPath(id))) - else None Project( id.toString, project.pkg.name, project.pkg.root.getAbsolutePath, - thumbUri.map(_.toString), + None, project.isPersistent ) } diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/main/MainModule.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/main/MainModule.scala new file mode 100644 index 0000000000..1e05571aae --- /dev/null +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/main/MainModule.scala @@ -0,0 +1,27 @@ +package org.enso.projectmanager.main + +import akka.actor.ActorSystem +import akka.stream.SystemMaterializer +import org.enso.jsonrpc.JsonRpcServer +import org.enso.projectmanager.main.configuration.ProjectManagerConfig +import org.enso.projectmanager.protocol.{ + JsonRpc, + ManagerClientControllerFactory +} + +/** + * A main module containing all components of the project manager. + * + * @param config a server config + */ +class MainModule(config: ProjectManagerConfig) { + + implicit val system = ActorSystem() + + implicit val materializer = SystemMaterializer.get(system) + + lazy val clientControllerFactory = new ManagerClientControllerFactory(system) + + lazy val server = new JsonRpcServer(JsonRpc.protocol, clientControllerFactory) + +} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/main/ProjectManager.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/main/ProjectManager.scala new file mode 100644 index 0000000000..d8d812867a --- /dev/null +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/main/ProjectManager.scala @@ -0,0 +1,43 @@ +package org.enso.projectmanager.main + +import com.typesafe.scalalogging.LazyLogging +import org.enso.projectmanager.main.configuration.ProjectManagerConfig +import pureconfig.ConfigSource + +import scala.concurrent.Await +import scala.io.StdIn +import scala.concurrent.duration._ +import pureconfig.generic.auto._ + +/** + * Project manager runner containing the main method. + */ +object ProjectManager extends App with LazyLogging { + + logger.info("Starting Language Server...") + + val config: ProjectManagerConfig = + ConfigSource + .resources("application.conf") + .withFallback(ConfigSource.systemProperties) + .at("project-manager") + .loadOrThrow[ProjectManagerConfig] + + val mainModule = new MainModule(config) + + val binding = + Await.result( + mainModule.server.bind(config.server.host, config.server.port), + 3.seconds + ) + + logger.info( + s"Started server at ${config.server.host}:${config.server.port}, press enter to kill server" + ) + StdIn.readLine() + logger.info("Stopping server...") + + binding.unbind() + mainModule.system.terminate() + +} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/main/configuration.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/main/configuration.scala new file mode 100644 index 0000000000..f82f9f9bd1 --- /dev/null +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/main/configuration.scala @@ -0,0 +1,20 @@ +package org.enso.projectmanager.main + +object configuration { + + /** + * A configuration object for properties of the Project Manager. + * + * @param server a JSON RPC server configuration + */ + case class ProjectManagerConfig(server: ServerConfig) + + /** + * A configuration object for properties of the JSON RPC server. + * + * @param host an address that the server listen on + * @param port a port that the server listen on + */ + case class ServerConfig(host: String, port: Int) + +} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ClientController.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ClientController.scala new file mode 100644 index 0000000000..7a0224ce82 --- /dev/null +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ClientController.scala @@ -0,0 +1,27 @@ +package org.enso.projectmanager.protocol + +import java.util.UUID + +import akka.actor.{Actor, Props} + +/** + * An actor handling communications between a single client and the project + * manager. + * + * @param clientId the internal client id. + */ +class ClientController(clientId: UUID) extends Actor { + override def receive: Receive = ??? +} + +object ClientController { + + /** + * Creates a configuration object used to create a [[ClientController]]. + * + * @param clientId the internal client id. + * @return a configuration object + */ + def props(clientId: UUID): Props = Props(new ClientController(clientId)) + +} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala new file mode 100644 index 0000000000..4d7df91326 --- /dev/null +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/JsonRpc.scala @@ -0,0 +1,16 @@ +package org.enso.projectmanager.protocol + +import io.circe.generic.auto._ +import org.enso.jsonrpc.Protocol +import org.enso.projectmanager.protocol.ProjectManagementApi.ProjectCreate + +object JsonRpc { + + /** + * A description of supported JSON RPC messages. + */ + lazy val protocol: Protocol = + Protocol.empty + .registerRequest(ProjectCreate) + +} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ManagerClientControllerFactory.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ManagerClientControllerFactory.scala new file mode 100644 index 0000000000..ed6b10c062 --- /dev/null +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ManagerClientControllerFactory.scala @@ -0,0 +1,25 @@ +package org.enso.projectmanager.protocol + +import java.util.UUID + +import akka.actor.{ActorRef, ActorSystem} +import org.enso.jsonrpc.ClientControllerFactory + +/** + * Project manager client controller factory. + * + * @param system the actor system + */ +class ManagerClientControllerFactory(system: ActorSystem) + extends ClientControllerFactory { + + /** + * Creates a client controller actor. + * + * @param clientId the internal client id. + * @return an actor ref to the client controller + */ + override def createClientController(clientId: UUID): ActorRef = + system.actorOf(ClientController.props(clientId)) + +} diff --git a/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala new file mode 100644 index 0000000000..4173912947 --- /dev/null +++ b/common/project-manager/src/main/scala/org/enso/projectmanager/protocol/ProjectManagementApi.scala @@ -0,0 +1,24 @@ +package org.enso.projectmanager.protocol + +import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused} + +/** + * The project management JSON RPC API provided by the project manager. + * See [[https://github.com/luna/enso/blob/master/doc/design/engine/engine-services.md]] + * for message specifications. + */ +object ProjectManagementApi { + + case object ProjectCreate extends Method("project/create") { + + case class Params(name: String) + + implicit val hasParams = new HasParams[this.type] { + type Params = ProjectCreate.Params + } + implicit val hasResult = new HasResult[this.type] { + type Result = Unused.type + } + } + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala b/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala index 3ae2b30bd9..ac98cd5bfd 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/MainModule.scala @@ -2,13 +2,11 @@ package org.enso.languageserver import java.io.File import java.net.URI -import java.nio.ByteBuffer -import java.util.UUID -import akka.actor.{ActorRef, ActorSystem, Props} +import akka.actor.{ActorSystem, Props} import akka.stream.SystemMaterializer import cats.effect.IO -import org.enso.languageserver +import org.enso.jsonrpc.JsonRpcServer import org.enso.languageserver.capability.CapabilityRouter import org.enso.languageserver.data.{ Config, @@ -16,9 +14,10 @@ import org.enso.languageserver.data.{ Sha3_224VersionCalculator } import org.enso.languageserver.filemanager.{FileSystem, FileSystemApi} +import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory} import org.enso.languageserver.runtime.RuntimeConnector import org.enso.languageserver.text.BufferRegistry -import org.enso.polyglot.{RuntimeApi, LanguageInfo, RuntimeServerInfo} +import org.enso.polyglot.{LanguageInfo, RuntimeServerInfo} import org.graalvm.polyglot.Context import org.graalvm.polyglot.io.MessageEndpoint @@ -74,11 +73,13 @@ class MainModule(serverConfig: LanguageServerConfig) { }) .build() + lazy val clientControllerFactory = new ServerClientControllerFactory( + languageServer, + bufferRegistry, + capabilityRouter + ) + lazy val server = - new WebSocketServer( - languageServer, - bufferRegistry, - capabilityRouter, - runtimeConnector - ) + new JsonRpcServer(JsonRpc.protocol, clientControllerFactory) + } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityApi.scala index cf98bf2fc8..d2cefe8670 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/capability/CapabilityApi.scala @@ -1,7 +1,7 @@ package org.enso.languageserver.capability import org.enso.languageserver.data.CapabilityRegistration -import org.enso.languageserver.jsonrpc.{HasParams, HasResult, Method, Unused} +import org.enso.jsonrpc.{HasParams, HasResult, Method, Unused} /** * The capability JSON RPC API provided by the language server. diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala index a6e8a24a70..08dd6bcbc1 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala @@ -1,12 +1,6 @@ package org.enso.languageserver.filemanager -import org.enso.languageserver.jsonrpc.{ - Error, - HasParams, - HasResult, - Method, - Unused -} +import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused} /** * The file manager JSON RPC API provided by the language server. diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemFailureMapper.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemFailureMapper.scala index 2968fe5069..3cb5cd7e1d 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemFailureMapper.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemFailureMapper.scala @@ -9,7 +9,7 @@ import org.enso.languageserver.filemanager.FileManagerApi.{ NotDirectoryError, OperationTimeoutError } -import org.enso.languageserver.jsonrpc.Error +import org.enso.jsonrpc.Error object FileSystemFailureMapper { diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/ClientController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ClientController.scala similarity index 86% rename from engine/language-server/src/main/scala/org/enso/languageserver/ClientController.scala rename to engine/language-server/src/main/scala/org/enso/languageserver/protocol/ClientController.scala index 189c8c93a0..2df2e1dc9a 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/ClientController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ClientController.scala @@ -1,8 +1,12 @@ -package org.enso.languageserver +package org.enso.languageserver.protocol + +import java.util.UUID import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash} import akka.pattern.ask import akka.util.Timeout +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ import org.enso.languageserver.capability.CapabilityApi.{ AcquireCapability, ForceReleaseCapability, @@ -21,64 +25,22 @@ import org.enso.languageserver.filemanager.{ FileManagerProtocol, FileSystemFailureMapper } -import org.enso.languageserver.jsonrpc.Errors.ServiceError -import org.enso.languageserver.jsonrpc._ -import org.enso.languageserver.requesthandler.{ - AcquireCapabilityHandler, - ApplyEditHandler, - CloseFileHandler, - OpenFileHandler, - ReleaseCapabilityHandler, - SaveFileHandler -} -import org.enso.languageserver.text.TextApi.{ - ApplyEdit, - CloseFile, - OpenFile, - SaveFile, - TextDidChange -} +import org.enso.languageserver.requesthandler._ +import org.enso.languageserver.text.TextApi._ import org.enso.languageserver.text.TextProtocol import scala.concurrent.duration._ import scala.util.{Failure, Success} -/** - * The JSON RPC API provided by the language server. - * See [[https://github.com/luna/enso/blob/master/doc/design/engine/engine-services.md]] - * for message specifications. - */ -object ClientApi { - import io.circe.generic.auto._ - - val protocol: Protocol = Protocol.empty - .registerRequest(AcquireCapability) - .registerRequest(ReleaseCapability) - .registerRequest(WriteFile) - .registerRequest(ReadFile) - .registerRequest(CreateFile) - .registerRequest(OpenFile) - .registerRequest(CloseFile) - .registerRequest(SaveFile) - .registerRequest(ApplyEdit) - .registerRequest(DeleteFile) - .registerRequest(CopyFile) - .registerRequest(MoveFile) - .registerRequest(ExistsFile) - .registerRequest(TreeFile) - .registerNotification(ForceReleaseCapability) - .registerNotification(GrantCapability) - .registerNotification(TextDidChange) - - case class WebConnect(webActor: ActorRef) -} - /** * An actor handling communications between a single client and the language * server. * * @param clientId the internal client id. - * @param server the language server actor. + * @param server the language server actor ref. + * @param bufferRegistry a router that dispatches text editing requests + * @param capabilityRouter a router that dispatches capability requests + * @param requestTimeout a request timeout */ class ClientController( val clientId: Client.Id, @@ -114,7 +76,7 @@ class ClientController( log.warning("Received unknown message: {}", message) override def receive: Receive = { - case ClientApi.WebConnect(webActor) => + case JsonRpcServer.WebConnect(webActor) => context.system.eventStream .publish(ClientConnected(Client(clientId, self))) unstashAll() @@ -343,3 +305,34 @@ class ClientController( } } + +object ClientController { + + /** + * Creates a configuration object used to create a [[ClientController]]. + * + * @param clientId the internal client id. + * @param server the language server actor ref. + * @param bufferRegistry a router that dispatches text editing requests + * @param capabilityRouter a router that dispatches capability requests + * @param requestTimeout a request timeout + * @return a configuration object + */ + def props( + clientId: UUID, + server: ActorRef, + bufferRegistry: ActorRef, + capabilityRouter: ActorRef, + requestTimeout: FiniteDuration = 10.seconds + ): Props = + Props( + new ClientController( + clientId, + server, + bufferRegistry, + capabilityRouter, + requestTimeout + ) + ) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/JsonRpc.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/JsonRpc.scala new file mode 100644 index 0000000000..216f299ca0 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/JsonRpc.scala @@ -0,0 +1,38 @@ +package org.enso.languageserver.protocol + +import io.circe.generic.auto._ +import org.enso.jsonrpc.Protocol +import org.enso.languageserver.capability.CapabilityApi.{ + AcquireCapability, + ForceReleaseCapability, + GrantCapability, + ReleaseCapability +} +import org.enso.languageserver.filemanager.FileManagerApi._ +import org.enso.languageserver.text.TextApi._ + +object JsonRpc { + + /** + * A description of supported JSON RPC messages. + */ + val protocol: Protocol = Protocol.empty + .registerRequest(AcquireCapability) + .registerRequest(ReleaseCapability) + .registerRequest(WriteFile) + .registerRequest(ReadFile) + .registerRequest(CreateFile) + .registerRequest(OpenFile) + .registerRequest(CloseFile) + .registerRequest(SaveFile) + .registerRequest(ApplyEdit) + .registerRequest(DeleteFile) + .registerRequest(CopyFile) + .registerRequest(MoveFile) + .registerRequest(ExistsFile) + .registerRequest(TreeFile) + .registerNotification(ForceReleaseCapability) + .registerNotification(GrantCapability) + .registerNotification(TextDidChange) + +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala new file mode 100644 index 0000000000..64c4c65863 --- /dev/null +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/ServerClientControllerFactory.scala @@ -0,0 +1,33 @@ +package org.enso.languageserver.protocol + +import java.util.UUID + +import akka.actor.{ActorRef, ActorSystem} +import org.enso.jsonrpc.ClientControllerFactory + +/** + * Language server client controller factory. + * + * @param server the language server actor ref + * @param bufferRegistry the buffer registry actor ref + * @param capabilityRouter the capability router actor ref + * @param system the actor system + */ +class ServerClientControllerFactory( + server: ActorRef, + bufferRegistry: ActorRef, + capabilityRouter: ActorRef +)(implicit system: ActorSystem) + extends ClientControllerFactory { + + /** + * Creates a client controller actor. + * + * @param clientId the internal client id. + * @return + */ + override def createClientController(clientId: UUID): ActorRef = + system.actorOf( + ClientController.props(clientId, server, bufferRegistry, capabilityRouter) + ) +} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/AcquireCapabilityHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/AcquireCapabilityHandler.scala index 3dfe282bd5..078a09bcf9 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/AcquireCapabilityHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/AcquireCapabilityHandler.scala @@ -1,6 +1,8 @@ package org.enso.languageserver.requesthandler import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ import org.enso.languageserver.capability.CapabilityApi.AcquireCapability import org.enso.languageserver.capability.CapabilityProtocol import org.enso.languageserver.capability.CapabilityProtocol.{ @@ -8,8 +10,6 @@ import org.enso.languageserver.capability.CapabilityProtocol.{ CapabilityAcquisitionBadRequest } import org.enso.languageserver.data.{CapabilityRegistration, Client} -import org.enso.languageserver.jsonrpc.Errors.ServiceError -import org.enso.languageserver.jsonrpc._ import scala.concurrent.duration.FiniteDuration diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ApplyEditHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ApplyEditHandler.scala index ac73efa499..802f910268 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ApplyEditHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ApplyEditHandler.scala @@ -1,24 +1,12 @@ package org.enso.languageserver.requesthandler import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ import org.enso.languageserver.data.Client -import org.enso.languageserver.jsonrpc.Errors.ServiceError -import org.enso.languageserver.jsonrpc._ -import org.enso.languageserver.text.TextApi.{ - ApplyEdit, - FileNotOpenedError, - InvalidVersionError, - TextEditValidationError, - WriteDeniedError -} +import org.enso.languageserver.text.TextApi._ import org.enso.languageserver.text.TextProtocol -import org.enso.languageserver.text.TextProtocol.{ - ApplyEditSuccess, - FileNotOpened, - TextEditInvalidVersion, - TextEditValidationFailed, - WriteDenied -} +import org.enso.languageserver.text.TextProtocol.{ApplyEdit => _, _} import scala.concurrent.duration.FiniteDuration diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/CloseFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/CloseFileHandler.scala index 7299749b97..125180e92e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/CloseFileHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/CloseFileHandler.scala @@ -1,9 +1,9 @@ package org.enso.languageserver.requesthandler import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ import org.enso.languageserver.data.Client -import org.enso.languageserver.jsonrpc.Errors.ServiceError -import org.enso.languageserver.jsonrpc._ import org.enso.languageserver.text.TextApi.{CloseFile, FileNotOpenedError} import org.enso.languageserver.text.TextProtocol import org.enso.languageserver.text.TextProtocol.{FileClosed, FileNotOpened} diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/OpenFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/OpenFileHandler.scala index 9b5f109f72..a35c9826b3 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/OpenFileHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/OpenFileHandler.scala @@ -1,15 +1,10 @@ package org.enso.languageserver.requesthandler import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc.{Id, Request, ResponseError, ResponseResult} import org.enso.languageserver.data.Client import org.enso.languageserver.filemanager.FileSystemFailureMapper -import org.enso.languageserver.jsonrpc.Errors.ServiceError -import org.enso.languageserver.jsonrpc.{ - Id, - Request, - ResponseError, - ResponseResult -} import org.enso.languageserver.text.TextApi.OpenFile import org.enso.languageserver.text.TextProtocol import org.enso.languageserver.text.TextProtocol.{ diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ReleaseCapabilityHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ReleaseCapabilityHandler.scala index 5c0e6a0ef8..afb2be3976 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ReleaseCapabilityHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/ReleaseCapabilityHandler.scala @@ -1,6 +1,8 @@ package org.enso.languageserver.requesthandler import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ import org.enso.languageserver.capability.CapabilityApi.ReleaseCapability import org.enso.languageserver.capability.CapabilityProtocol import org.enso.languageserver.capability.CapabilityProtocol.{ @@ -8,8 +10,6 @@ import org.enso.languageserver.capability.CapabilityProtocol.{ CapabilityReleased } import org.enso.languageserver.data.{CapabilityRegistration, Client} -import org.enso.languageserver.jsonrpc.Errors.ServiceError -import org.enso.languageserver.jsonrpc._ import scala.concurrent.duration.FiniteDuration diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/SaveFileHandler.scala b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/SaveFileHandler.scala index 32ee8332be..5794fc93ac 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/SaveFileHandler.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/requesthandler/SaveFileHandler.scala @@ -1,10 +1,10 @@ package org.enso.languageserver.requesthandler import akka.actor.{Actor, ActorLogging, ActorRef, Props} +import org.enso.jsonrpc.Errors.ServiceError +import org.enso.jsonrpc._ import org.enso.languageserver.data.Client import org.enso.languageserver.filemanager.FileSystemFailureMapper -import org.enso.languageserver.jsonrpc.Errors.ServiceError -import org.enso.languageserver.jsonrpc._ import org.enso.languageserver.text.TextApi.{ FileNotOpenedError, InvalidVersionError, @@ -12,13 +12,7 @@ import org.enso.languageserver.text.TextApi.{ WriteDeniedError } import org.enso.languageserver.text.TextProtocol -import org.enso.languageserver.text.TextProtocol.{ - FileNotOpened, - FileSaved, - SaveDenied, - SaveFailed, - SaveFileInvalidVersion -} +import org.enso.languageserver.text.TextProtocol._ import scala.concurrent.duration.FiniteDuration diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/text/TextApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/text/TextApi.scala index b0ddd9099e..5faf6d1833 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/text/TextApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/text/TextApi.scala @@ -2,13 +2,7 @@ package org.enso.languageserver.text import org.enso.languageserver.data.CapabilityRegistration import org.enso.languageserver.filemanager.Path -import org.enso.languageserver.jsonrpc.{ - Error, - HasParams, - HasResult, - Method, - Unused -} +import org.enso.jsonrpc.{Error, HasParams, HasResult, Method, Unused} import org.enso.languageserver.text.editing.model.FileEdit /** diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala new file mode 100644 index 0000000000..3613c9cac0 --- /dev/null +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/BaseServerTest.scala @@ -0,0 +1,48 @@ +package org.enso.languageserver.websocket + +import java.nio.file.Files +import java.util.UUID + +import akka.actor.Props +import cats.effect.IO +import org.enso.jsonrpc.{ClientControllerFactory, Protocol} +import org.enso.jsonrpc.test.JsonRpcServerTestKit +import org.enso.languageserver.{LanguageProtocol, LanguageServer} +import org.enso.languageserver.capability.CapabilityRouter +import org.enso.languageserver.data.{Config, Sha3_224VersionCalculator} +import org.enso.languageserver.filemanager.FileSystem +import org.enso.languageserver.protocol.{JsonRpc, ServerClientControllerFactory} +import org.enso.languageserver.text.BufferRegistry + +class BaseServerTest extends JsonRpcServerTestKit { + + val testContentRoot = Files.createTempDirectory(null) + val testContentRootId = UUID.randomUUID() + val config = Config(Map(testContentRootId -> testContentRoot.toFile)) + + testContentRoot.toFile.deleteOnExit() + + override def protocol: Protocol = JsonRpc.protocol + + override def clientControllerFactory: ClientControllerFactory = { + val languageServer = + system.actorOf( + Props(new LanguageServer(config, new FileSystem[IO])) + ) + languageServer ! LanguageProtocol.Initialize + val bufferRegistry = + system.actorOf( + BufferRegistry.props(languageServer)(Sha3_224VersionCalculator) + ) + + lazy val capabilityRouter = + system.actorOf(CapabilityRouter.props(bufferRegistry)) + + new ServerClientControllerFactory( + languageServer, + bufferRegistry, + capabilityRouter + ) + } + +} diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala index 338c8b167d..f61b90a13c 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala @@ -6,7 +6,7 @@ import java.util.UUID import io.circe.literal._ import org.apache.commons.io.FileUtils -class FileManagerTest extends WebSocketServerTest { +class FileManagerTest extends BaseServerTest { "File Server" must { "write textual content to a file" in { diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/TextOperationsTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/TextOperationsTest.scala index ddb880185f..511a377114 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/TextOperationsTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/TextOperationsTest.scala @@ -5,7 +5,7 @@ import io.circe.literal._ import org.enso.languageserver.event.BufferClosed import org.enso.languageserver.filemanager.Path -class TextOperationsTest extends WebSocketServerTest { +class TextOperationsTest extends BaseServerTest { "text/openFile" must { "fail opening a file if it does not exist" in { diff --git a/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala b/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala index 8c5b63c715..59525df290 100644 --- a/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala +++ b/engine/runner/src/main/scala/org/enso/runner/LanguageServerApp.scala @@ -1,21 +1,10 @@ package org.enso.runner -import java.io.File - -import akka.actor.{ActorSystem, Props} -import akka.stream.{ActorMaterializer, SystemMaterializer} -import cats.effect.IO -import org.enso.interpreter.instrument.ReplDebuggerInstrument -import org.enso.languageserver.data.Config -import org.enso.languageserver.filemanager.FileSystem import org.enso.languageserver.{ LanguageProtocol, - LanguageServer, LanguageServerConfig, - MainModule, - WebSocketServer + MainModule } -import org.enso.polyglot.LanguageInfo import scala.concurrent.Await import scala.concurrent.duration._ @@ -47,6 +36,7 @@ object LanguageServerApp { s"Started server at ${config.interface}:${config.port}, press enter to kill server" ) StdIn.readLine() + binding.terminate(10.seconds) } }