mirror of
https://github.com/enso-org/enso.git
synced 2024-11-30 05:13:52 +03:00
text/openFile method (#575)
This commit is contained in:
parent
9913915fd9
commit
e5530045bf
@ -311,10 +311,38 @@ Detailed information on the flags it supports can be obtained by executing `run
|
|||||||
project.
|
project.
|
||||||
|
|
||||||
#### Language Server Mode
|
#### Language Server Mode
|
||||||
Though operating the Enso binary as a language server is functionality planned
|
The Language Server can be run using the `--server` option. It requires also a
|
||||||
for the 2.0 release, it is not currently implemented. For more information on
|
content root to be provided (`--root-id` and `--path` options). Command-line
|
||||||
the planned functionality and its progress, please see the
|
interface of the runner prints all server options when you execute it with
|
||||||
[Issue Tracker](https://github.com/luna/enso/issues).
|
`--help` option.
|
||||||
|
|
||||||
|
Below are options uses by the Language Server:
|
||||||
|
- `--server`: Runs the Language Server
|
||||||
|
- `--root-id <uuid>`: Content root id.
|
||||||
|
- `--path <path>`: Path to the content root.
|
||||||
|
- `--interface <interface>`: Interface for processing all incoming connections.
|
||||||
|
Default value is 127.0.0.1
|
||||||
|
- `--port <port>`: Port for processing all incoming connections. Default value
|
||||||
|
is 8080.
|
||||||
|
|
||||||
|
To run the Language Server on 127.0.0.1:8080 type:
|
||||||
|
```bash
|
||||||
|
java -jar enso.jar \
|
||||||
|
--server \
|
||||||
|
--root-id 3256d10d-45be-45b1-9ea4-7912ef4226b1 \
|
||||||
|
--path /tmp/content-root
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to provide a socket that the server should listen to, type:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
java -jar enso.jar \
|
||||||
|
--server \
|
||||||
|
--root-id 3256d10d-45be-45b1-9ea4-7912ef4226b1 \
|
||||||
|
--path /tmp/content-root \
|
||||||
|
--interface 0.0.0.0 \
|
||||||
|
--port 80
|
||||||
|
```
|
||||||
|
|
||||||
## Pull Requests
|
## Pull Requests
|
||||||
Pull Requests are the primary method for making changes to Enso. GitHub has
|
Pull Requests are the primary method for making changes to Enso. GitHub has
|
||||||
|
15
build.sbt
15
build.sbt
@ -429,6 +429,7 @@ lazy val language_server = (project in file("engine/language-server"))
|
|||||||
"io.circe" %% "circe-literal" % circeVersion,
|
"io.circe" %% "circe-literal" % circeVersion,
|
||||||
"org.typelevel" %% "cats-core" % "2.0.0",
|
"org.typelevel" %% "cats-core" % "2.0.0",
|
||||||
"org.typelevel" %% "cats-effect" % "2.0.0",
|
"org.typelevel" %% "cats-effect" % "2.0.0",
|
||||||
|
"org.bouncycastle" % "bcpkix-jdk15on" % "1.64",
|
||||||
"commons-io" % "commons-io" % "2.6",
|
"commons-io" % "commons-io" % "2.6",
|
||||||
akkaTestkit % Test,
|
akkaTestkit % Test,
|
||||||
"org.scalatest" %% "scalatest" % "3.2.0-M2" % Test,
|
"org.scalatest" %% "scalatest" % "3.2.0-M2" % Test,
|
||||||
@ -544,6 +545,20 @@ lazy val runner = project
|
|||||||
assemblyJarName in assembly := "enso.jar",
|
assemblyJarName in assembly := "enso.jar",
|
||||||
test in assembly := {},
|
test in assembly := {},
|
||||||
assemblyOutputPath in assembly := file("enso.jar"),
|
assemblyOutputPath in assembly := file("enso.jar"),
|
||||||
|
assemblyMergeStrategy in assembly := {
|
||||||
|
case PathList("META-INF", file, xs @ _*) if file.endsWith(".DSA") =>
|
||||||
|
MergeStrategy.discard
|
||||||
|
case PathList("META-INF", file, xs @ _*) if file.endsWith(".SF") =>
|
||||||
|
MergeStrategy.discard
|
||||||
|
case PathList("META-INF", "MANIFEST.MF", xs @ _*) =>
|
||||||
|
MergeStrategy.discard
|
||||||
|
case "application.conf" =>
|
||||||
|
MergeStrategy.concat
|
||||||
|
case "reference.conf" =>
|
||||||
|
MergeStrategy.concat
|
||||||
|
case x =>
|
||||||
|
MergeStrategy.first
|
||||||
|
},
|
||||||
assemblyOption in assembly := (assemblyOption in assembly).value
|
assemblyOption in assembly := (assemblyOption in assembly).value
|
||||||
.copy(
|
.copy(
|
||||||
prependShellScript = Some(
|
prependShellScript = Some(
|
||||||
|
@ -13,8 +13,7 @@ import java.util.UUID
|
|||||||
|
|
||||||
import org.apache.commons.io.FileUtils
|
import org.apache.commons.io.FileUtils
|
||||||
import org.enso.FileManager
|
import org.enso.FileManager
|
||||||
import org.scalatest.BeforeAndAfterAll
|
import org.scalatest.{BeforeAndAfterAll, Ignore, Outcome}
|
||||||
import org.scalatest.Outcome
|
|
||||||
import org.scalatest.funsuite.AnyFunSuite
|
import org.scalatest.funsuite.AnyFunSuite
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
|
|
||||||
@ -25,6 +24,7 @@ import scala.reflect.ClassTag
|
|||||||
import scala.util.Try
|
import scala.util.Try
|
||||||
|
|
||||||
// needs to be separate because watcher message are asynchronous
|
// needs to be separate because watcher message are asynchronous
|
||||||
|
@Ignore
|
||||||
class WatchTests
|
class WatchTests
|
||||||
extends AnyFunSuite
|
extends AnyFunSuite
|
||||||
with BeforeAndAfterAll
|
with BeforeAndAfterAll
|
||||||
|
@ -927,8 +927,8 @@ A representation of a batch of edits to a file, versioned.
|
|||||||
interface FileEdit {
|
interface FileEdit {
|
||||||
path: Path;
|
path: Path;
|
||||||
edits: [TextEdit];
|
edits: [TextEdit];
|
||||||
oldVersion: UUID;
|
oldVersion: SHA3-224;
|
||||||
newVersion: UUID;
|
newVersion: SHA3-224;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -995,7 +995,6 @@ client.
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface CapabilityRegistration {
|
interface CapabilityRegistration {
|
||||||
id: UUID; // The registration ID
|
|
||||||
method: String;
|
method: String;
|
||||||
registerOptions?: any;
|
registerOptions?: any;
|
||||||
}
|
}
|
||||||
@ -1024,7 +1023,7 @@ capability.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
id: UUID; // The ID used to register the capability
|
registration: CapabilityRegistration;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1066,7 +1065,7 @@ capability set.
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
id: UUID; // The ID used to register the capability
|
registration: CapabilityRegistration;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1567,7 +1566,7 @@ the client that sent the `text/openFile` message.
|
|||||||
{
|
{
|
||||||
writeCapability?: CapabilityRegistration;
|
writeCapability?: CapabilityRegistration;
|
||||||
content: String;
|
content: String;
|
||||||
currentVersion: UUID;
|
currentVersion: SHA3-224;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -1612,7 +1611,7 @@ that file, or if the client is requesting a save of an outdated version.
|
|||||||
```typescript
|
```typescript
|
||||||
{
|
{
|
||||||
path: Path;
|
path: Path;
|
||||||
currentVersion: UUID;
|
currentVersion: SHA3-224;
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -2022,6 +2021,16 @@ It signals that file already exists.
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### `OperationTimeoutError`
|
||||||
|
It signals that IO operation timed out.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
"error" : {
|
||||||
|
"code" : 1005,
|
||||||
|
"message" : "IO operation timeout"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
##### `StackItemNotFoundError`
|
##### `StackItemNotFoundError`
|
||||||
```typescript
|
```typescript
|
||||||
"error" : {
|
"error" : {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package org.enso.languageserver.buffer
|
package org.enso.languageserver.text
|
||||||
import org.enso.languageserver.data.buffer.Rope
|
import org.enso.languageserver.data.buffer.Rope
|
||||||
import org.scalacheck.Gen.Parameters
|
import org.scalacheck.Gen.Parameters
|
||||||
import org.scalameter.{Bench, Gen}
|
import org.scalameter.{Bench, Gen}
|
@ -1,19 +1,34 @@
|
|||||||
package org.enso.languageserver
|
package org.enso.languageserver
|
||||||
|
|
||||||
import java.util.UUID
|
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
|
||||||
|
|
||||||
import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
|
|
||||||
import akka.pattern.ask
|
import akka.pattern.ask
|
||||||
import akka.util.Timeout
|
import akka.util.Timeout
|
||||||
import org.enso.languageserver.ClientApi._
|
import org.enso.languageserver.capability.CapabilityApi.{
|
||||||
import org.enso.languageserver.data.{CapabilityRegistration, Client}
|
AcquireCapability,
|
||||||
|
ForceReleaseCapability,
|
||||||
|
GrantCapability,
|
||||||
|
ReleaseCapability
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol
|
||||||
|
import org.enso.languageserver.data.Client
|
||||||
|
import org.enso.languageserver.event.{ClientConnected, ClientDisconnected}
|
||||||
import org.enso.languageserver.filemanager.FileManagerApi._
|
import org.enso.languageserver.filemanager.FileManagerApi._
|
||||||
|
import org.enso.languageserver.filemanager.FileManagerProtocol.{
|
||||||
|
CreateFileResult,
|
||||||
|
WriteFileResult
|
||||||
|
}
|
||||||
import org.enso.languageserver.filemanager.{
|
import org.enso.languageserver.filemanager.{
|
||||||
FileManagerProtocol,
|
FileManagerProtocol,
|
||||||
FileSystemFailureMapper
|
FileSystemFailureMapper
|
||||||
}
|
}
|
||||||
import org.enso.languageserver.jsonrpc.Errors.ServiceError
|
import org.enso.languageserver.jsonrpc.Errors.ServiceError
|
||||||
import org.enso.languageserver.jsonrpc._
|
import org.enso.languageserver.jsonrpc._
|
||||||
|
import org.enso.languageserver.requesthandler.{
|
||||||
|
AcquireCapabilityHandler,
|
||||||
|
OpenFileHandler,
|
||||||
|
ReleaseCapabilityHandler
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.text.TextApi.OpenFile
|
||||||
|
|
||||||
import scala.concurrent.duration._
|
import scala.concurrent.duration._
|
||||||
import scala.util.{Failure, Success}
|
import scala.util.{Failure, Success}
|
||||||
@ -26,45 +41,13 @@ import scala.util.{Failure, Success}
|
|||||||
object ClientApi {
|
object ClientApi {
|
||||||
import io.circe.generic.auto._
|
import io.circe.generic.auto._
|
||||||
|
|
||||||
case object AcquireCapability extends Method("capability/acquire") {
|
|
||||||
implicit val hasParams = new HasParams[this.type] {
|
|
||||||
type Params = CapabilityRegistration
|
|
||||||
}
|
|
||||||
implicit val hasResult = new HasResult[this.type] {
|
|
||||||
type Result = Unused.type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case class ReleaseCapabilityParams(id: UUID)
|
|
||||||
|
|
||||||
case object ReleaseCapability extends Method("capability/release") {
|
|
||||||
implicit val hasParams = new HasParams[this.type] {
|
|
||||||
type Params = ReleaseCapabilityParams
|
|
||||||
}
|
|
||||||
implicit val hasResult = new HasResult[this.type] {
|
|
||||||
type Result = Unused.type
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case object ForceReleaseCapability
|
|
||||||
extends Method("capability/forceReleased") {
|
|
||||||
implicit val hasParams = new HasParams[this.type] {
|
|
||||||
type Params = ReleaseCapabilityParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
case object GrantCapability extends Method("capability/granted") {
|
|
||||||
implicit val hasParams = new HasParams[this.type] {
|
|
||||||
type Params = CapabilityRegistration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val protocol: Protocol = Protocol.empty
|
val protocol: Protocol = Protocol.empty
|
||||||
.registerRequest(AcquireCapability)
|
.registerRequest(AcquireCapability)
|
||||||
.registerRequest(ReleaseCapability)
|
.registerRequest(ReleaseCapability)
|
||||||
.registerRequest(WriteFile)
|
.registerRequest(WriteFile)
|
||||||
.registerRequest(ReadFile)
|
.registerRequest(ReadFile)
|
||||||
.registerRequest(CreateFile)
|
.registerRequest(CreateFile)
|
||||||
|
.registerRequest(OpenFile)
|
||||||
.registerRequest(DeleteFile)
|
.registerRequest(DeleteFile)
|
||||||
.registerRequest(CopyFile)
|
.registerRequest(CopyFile)
|
||||||
.registerNotification(ForceReleaseCapability)
|
.registerNotification(ForceReleaseCapability)
|
||||||
@ -83,6 +66,8 @@ object ClientApi {
|
|||||||
class ClientController(
|
class ClientController(
|
||||||
val clientId: Client.Id,
|
val clientId: Client.Id,
|
||||||
val server: ActorRef,
|
val server: ActorRef,
|
||||||
|
val bufferRegistry: ActorRef,
|
||||||
|
val capabilityRouter: ActorRef,
|
||||||
requestTimeout: FiniteDuration = 10.seconds
|
requestTimeout: FiniteDuration = 10.seconds
|
||||||
) extends Actor
|
) extends Actor
|
||||||
with Stash
|
with Stash
|
||||||
@ -92,8 +77,21 @@ class ClientController(
|
|||||||
|
|
||||||
implicit val timeout = Timeout(requestTimeout)
|
implicit val timeout = Timeout(requestTimeout)
|
||||||
|
|
||||||
|
private val client = Client(clientId, self)
|
||||||
|
|
||||||
|
private val requestHandlers: Map[Method, Props] =
|
||||||
|
Map(
|
||||||
|
AcquireCapability -> AcquireCapabilityHandler
|
||||||
|
.props(capabilityRouter, requestTimeout, client),
|
||||||
|
ReleaseCapability -> ReleaseCapabilityHandler
|
||||||
|
.props(capabilityRouter, requestTimeout, client),
|
||||||
|
OpenFile -> OpenFileHandler.props(bufferRegistry, requestTimeout, client)
|
||||||
|
)
|
||||||
|
|
||||||
override def receive: Receive = {
|
override def receive: Receive = {
|
||||||
case ClientApi.WebConnect(webActor) =>
|
case ClientApi.WebConnect(webActor) =>
|
||||||
|
context.system.eventStream
|
||||||
|
.publish(ClientConnected(Client(clientId, self)))
|
||||||
unstashAll()
|
unstashAll()
|
||||||
context.become(connected(webActor))
|
context.become(connected(webActor))
|
||||||
case _ => stash()
|
case _ => stash()
|
||||||
@ -101,25 +99,18 @@ class ClientController(
|
|||||||
|
|
||||||
def connected(webActor: ActorRef): Receive = {
|
def connected(webActor: ActorRef): Receive = {
|
||||||
case MessageHandler.Disconnected =>
|
case MessageHandler.Disconnected =>
|
||||||
server ! LanguageProtocol.Disconnect(clientId)
|
context.system.eventStream.publish(ClientDisconnected(clientId))
|
||||||
context.stop(self)
|
context.stop(self)
|
||||||
|
|
||||||
case LanguageProtocol.CapabilityForceReleased(id) =>
|
case CapabilityProtocol.CapabilityForceReleased(registration) =>
|
||||||
webActor ! Notification(
|
webActor ! Notification(ForceReleaseCapability, registration)
|
||||||
ForceReleaseCapability,
|
|
||||||
ReleaseCapabilityParams(id)
|
|
||||||
)
|
|
||||||
|
|
||||||
case LanguageProtocol.CapabilityGranted(registration) =>
|
case CapabilityProtocol.CapabilityGranted(registration) =>
|
||||||
webActor ! Notification(GrantCapability, registration)
|
webActor ! Notification(GrantCapability, registration)
|
||||||
|
|
||||||
case Request(AcquireCapability, id, registration: CapabilityRegistration) =>
|
case r @ Request(method, _, _) if (requestHandlers.contains(method)) =>
|
||||||
server ! LanguageProtocol.AcquireCapability(clientId, registration)
|
val handler = context.actorOf(requestHandlers(method))
|
||||||
sender ! ResponseResult(AcquireCapability, id, Unused)
|
handler.forward(r)
|
||||||
|
|
||||||
case Request(ReleaseCapability, id, params: ReleaseCapabilityParams) =>
|
|
||||||
server ! LanguageProtocol.ReleaseCapability(clientId, params.id)
|
|
||||||
sender ! ResponseResult(ReleaseCapability, id, Unused)
|
|
||||||
|
|
||||||
case Request(WriteFile, id, params: WriteFile.Params) =>
|
case Request(WriteFile, id, params: WriteFile.Params) =>
|
||||||
writeFile(webActor, id, params)
|
writeFile(webActor, id, params)
|
||||||
@ -167,10 +158,10 @@ class ClientController(
|
|||||||
): Unit = {
|
): Unit = {
|
||||||
(server ? FileManagerProtocol.WriteFile(params.path, params.contents))
|
(server ? FileManagerProtocol.WriteFile(params.path, params.contents))
|
||||||
.onComplete {
|
.onComplete {
|
||||||
case Success(FileManagerProtocol.WriteFileResult(Right(()))) =>
|
case Success(WriteFileResult(Right(()))) =>
|
||||||
webActor ! ResponseResult(WriteFile, id, Unused)
|
webActor ! ResponseResult(WriteFile, id, Unused)
|
||||||
|
|
||||||
case Success(FileManagerProtocol.WriteFileResult(Left(failure))) =>
|
case Success(WriteFileResult(Left(failure))) =>
|
||||||
webActor ! ResponseError(
|
webActor ! ResponseError(
|
||||||
Some(id),
|
Some(id),
|
||||||
FileSystemFailureMapper.mapFailure(failure)
|
FileSystemFailureMapper.mapFailure(failure)
|
||||||
@ -189,10 +180,10 @@ class ClientController(
|
|||||||
): Unit = {
|
): Unit = {
|
||||||
(server ? FileManagerProtocol.CreateFile(params.`object`))
|
(server ? FileManagerProtocol.CreateFile(params.`object`))
|
||||||
.onComplete {
|
.onComplete {
|
||||||
case Success(FileManagerProtocol.CreateFileResult(Right(()))) =>
|
case Success(CreateFileResult(Right(()))) =>
|
||||||
webActor ! ResponseResult(CreateFile, id, Unused)
|
webActor ! ResponseResult(CreateFile, id, Unused)
|
||||||
|
|
||||||
case Success(FileManagerProtocol.CreateFileResult(Left(failure))) =>
|
case Success(CreateFileResult(Left(failure))) =>
|
||||||
webActor ! ResponseError(
|
webActor ! ResponseError(
|
||||||
Some(id),
|
Some(id),
|
||||||
FileSystemFailureMapper.mapFailure(failure)
|
FileSystemFailureMapper.mapFailure(failure)
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
package org.enso.languageserver
|
package org.enso.languageserver
|
||||||
|
|
||||||
import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
|
import akka.actor.{Actor, ActorLogging, Stash}
|
||||||
import cats.effect.IO
|
import cats.effect.IO
|
||||||
import org.enso.languageserver.data._
|
import org.enso.languageserver.data._
|
||||||
|
import org.enso.languageserver.event.{
|
||||||
|
ClientConnected,
|
||||||
|
ClientDisconnected,
|
||||||
|
ClientEvent
|
||||||
|
}
|
||||||
import org.enso.languageserver.filemanager.FileManagerProtocol._
|
import org.enso.languageserver.filemanager.FileManagerProtocol._
|
||||||
import org.enso.languageserver.filemanager.{FileSystemApi, FileSystemObject}
|
import org.enso.languageserver.filemanager.{FileSystemApi, FileSystemObject}
|
||||||
|
|
||||||
@ -11,59 +16,6 @@ object LanguageProtocol {
|
|||||||
/** Initializes the Language Server. */
|
/** Initializes the Language Server. */
|
||||||
case object Initialize
|
case object Initialize
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the Language Server about a new client connecting.
|
|
||||||
*
|
|
||||||
* @param clientId the internal client id.
|
|
||||||
* @param clientActor the actor this client is represented by.
|
|
||||||
*/
|
|
||||||
case class Connect(clientId: Client.Id, clientActor: ActorRef)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the Language Server about a client disconnecting.
|
|
||||||
* The client may not send any further messages after this one.
|
|
||||||
*
|
|
||||||
* @param clientId the id of the disconnecting client.
|
|
||||||
*/
|
|
||||||
case class Disconnect(clientId: Client.Id)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests the Language Server grant a new capability to a client.
|
|
||||||
*
|
|
||||||
* @param clientId the client to grant the capability to.
|
|
||||||
* @param registration the capability to grant.
|
|
||||||
*/
|
|
||||||
case class AcquireCapability(
|
|
||||||
clientId: Client.Id,
|
|
||||||
registration: CapabilityRegistration
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Notifies the Language Server about a client releasing a capability.
|
|
||||||
*
|
|
||||||
* @param clientId the client releasing the capability.
|
|
||||||
* @param capabilityId the capability being released.
|
|
||||||
*/
|
|
||||||
case class ReleaseCapability(
|
|
||||||
clientId: Client.Id,
|
|
||||||
capabilityId: CapabilityRegistration.Id
|
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A notification sent by the Language Server, notifying a client about
|
|
||||||
* a capability being taken away from them.
|
|
||||||
*
|
|
||||||
* @param capabilityId the capability being released.
|
|
||||||
*/
|
|
||||||
case class CapabilityForceReleased(capabilityId: CapabilityRegistration.Id)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A notification sent by the Language Server, notifying a client about a new
|
|
||||||
* capability being granted to them.
|
|
||||||
*
|
|
||||||
* @param registration the capability being granted.
|
|
||||||
*/
|
|
||||||
case class CapabilityGranted(registration: CapabilityRegistration)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,6 +29,10 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO])
|
|||||||
with ActorLogging {
|
with ActorLogging {
|
||||||
import LanguageProtocol._
|
import LanguageProtocol._
|
||||||
|
|
||||||
|
override def preStart(): Unit = {
|
||||||
|
context.system.eventStream.subscribe(self, classOf[ClientEvent])
|
||||||
|
}
|
||||||
|
|
||||||
override def receive: Receive = {
|
override def receive: Receive = {
|
||||||
case Initialize =>
|
case Initialize =>
|
||||||
log.debug("Language Server initialized.")
|
log.debug("Language Server initialized.")
|
||||||
@ -89,38 +45,16 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO])
|
|||||||
config: Config,
|
config: Config,
|
||||||
env: Environment = Environment.empty
|
env: Environment = Environment.empty
|
||||||
): Receive = {
|
): Receive = {
|
||||||
case Connect(clientId, actor) =>
|
case ClientConnected(client) =>
|
||||||
log.debug("Client connected [{}].", clientId)
|
log.info("Client connected [{}].", client.id)
|
||||||
context.become(
|
context.become(
|
||||||
initialized(config, env.addClient(Client(clientId, actor)))
|
initialized(config, env.addClient(client))
|
||||||
)
|
)
|
||||||
|
|
||||||
case Disconnect(clientId) =>
|
case ClientDisconnected(clientId) =>
|
||||||
log.debug("Client disconnected [{}].", clientId)
|
log.info("Client disconnected [{}].", clientId)
|
||||||
context.become(initialized(config, env.removeClient(clientId)))
|
context.become(initialized(config, env.removeClient(clientId)))
|
||||||
|
|
||||||
case AcquireCapability(
|
|
||||||
clientId,
|
|
||||||
reg @ CapabilityRegistration(_, capability: CanEdit)
|
|
||||||
) =>
|
|
||||||
val (envWithoutCapability, releasingClients) = env.removeCapabilitiesBy {
|
|
||||||
case CapabilityRegistration(_, CanEdit(file)) => file == capability.path
|
|
||||||
case _ => false
|
|
||||||
}
|
|
||||||
releasingClients.foreach {
|
|
||||||
case (client: Client, capabilities) =>
|
|
||||||
capabilities.foreach { registration =>
|
|
||||||
client.actor ! CapabilityForceReleased(registration.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val newEnv = envWithoutCapability.grantCapability(clientId, reg)
|
|
||||||
context.become(initialized(config, newEnv))
|
|
||||||
|
|
||||||
case ReleaseCapability(clientId, capabilityId) =>
|
|
||||||
context.become(
|
|
||||||
initialized(config, env.releaseCapability(clientId, capabilityId))
|
|
||||||
)
|
|
||||||
|
|
||||||
case WriteFile(path, content) =>
|
case WriteFile(path, content) =>
|
||||||
val result =
|
val result =
|
||||||
for {
|
for {
|
||||||
|
@ -0,0 +1,18 @@
|
|||||||
|
package org.enso.languageserver
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The config of the running Language Server instance.
|
||||||
|
*
|
||||||
|
* @param interface a interface that the server listen to
|
||||||
|
* @param port a port that the server listen to
|
||||||
|
* @param contentRootUuid an id of content root
|
||||||
|
* @param contentRootPath a path to the content root
|
||||||
|
*/
|
||||||
|
case class LanguageServerConfig(
|
||||||
|
interface: String,
|
||||||
|
port: Int,
|
||||||
|
contentRootUuid: UUID,
|
||||||
|
contentRootPath: String
|
||||||
|
)
|
@ -0,0 +1,52 @@
|
|||||||
|
package org.enso.languageserver
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
import akka.actor.{ActorSystem, Props}
|
||||||
|
import akka.stream.SystemMaterializer
|
||||||
|
import cats.effect.IO
|
||||||
|
import org.enso.languageserver.capability.CapabilityRouter
|
||||||
|
import org.enso.languageserver.data.{
|
||||||
|
Config,
|
||||||
|
ContentBasedVersioning,
|
||||||
|
Sha3_224VersionCalculator
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.filemanager.{FileSystem, FileSystemApi}
|
||||||
|
import org.enso.languageserver.text.BufferRegistry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A main module containing all components of th server.
|
||||||
|
*
|
||||||
|
* @param serverConfig a server config
|
||||||
|
*/
|
||||||
|
class MainModule(serverConfig: LanguageServerConfig) {
|
||||||
|
|
||||||
|
lazy val languageServerConfig = Config(
|
||||||
|
Map(serverConfig.contentRootUuid -> new File(serverConfig.contentRootPath))
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val fileSystem: FileSystemApi[IO] = new FileSystem[IO]
|
||||||
|
|
||||||
|
implicit val versionCalculator: ContentBasedVersioning =
|
||||||
|
Sha3_224VersionCalculator
|
||||||
|
|
||||||
|
implicit val system = ActorSystem()
|
||||||
|
|
||||||
|
implicit val materializer = SystemMaterializer.get(system)
|
||||||
|
|
||||||
|
lazy val languageServer =
|
||||||
|
system.actorOf(
|
||||||
|
Props(new LanguageServer(languageServerConfig, fileSystem)),
|
||||||
|
"server"
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val bufferRegistry =
|
||||||
|
system.actorOf(BufferRegistry.props(languageServer), "buffer-registry")
|
||||||
|
|
||||||
|
lazy val capabilityRouter =
|
||||||
|
system.actorOf(CapabilityRouter.props(bufferRegistry), "capability-router")
|
||||||
|
|
||||||
|
lazy val server =
|
||||||
|
new WebSocketServer(languageServer, bufferRegistry, capabilityRouter)
|
||||||
|
|
||||||
|
}
|
@ -12,9 +12,8 @@ import akka.stream.scaladsl.{Flow, Sink, Source}
|
|||||||
import akka.stream.{Materializer, OverflowStrategy}
|
import akka.stream.{Materializer, OverflowStrategy}
|
||||||
import org.enso.languageserver.jsonrpc.MessageHandler
|
import org.enso.languageserver.jsonrpc.MessageHandler
|
||||||
|
|
||||||
|
import scala.concurrent.duration.{FiniteDuration, _}
|
||||||
import scala.concurrent.{ExecutionContext, Future}
|
import scala.concurrent.{ExecutionContext, Future}
|
||||||
import scala.concurrent.duration.FiniteDuration
|
|
||||||
import scala.concurrent.duration._
|
|
||||||
|
|
||||||
object WebSocketServer {
|
object WebSocketServer {
|
||||||
|
|
||||||
@ -47,6 +46,8 @@ object WebSocketServer {
|
|||||||
*/
|
*/
|
||||||
class WebSocketServer(
|
class WebSocketServer(
|
||||||
languageServer: ActorRef,
|
languageServer: ActorRef,
|
||||||
|
bufferRegistry: ActorRef,
|
||||||
|
capabilityRouter: ActorRef,
|
||||||
config: WebSocketServer.Config = WebSocketServer.Config.default
|
config: WebSocketServer.Config = WebSocketServer.Config.default
|
||||||
)(
|
)(
|
||||||
implicit val system: ActorSystem,
|
implicit val system: ActorSystem,
|
||||||
@ -60,7 +61,16 @@ class WebSocketServer(
|
|||||||
private def newUser(): Flow[Message, Message, NotUsed] = {
|
private def newUser(): Flow[Message, Message, NotUsed] = {
|
||||||
val clientId = UUID.randomUUID()
|
val clientId = UUID.randomUUID()
|
||||||
val clientActor =
|
val clientActor =
|
||||||
system.actorOf(Props(new ClientController(clientId, languageServer)))
|
system.actorOf(
|
||||||
|
Props(
|
||||||
|
new ClientController(
|
||||||
|
clientId,
|
||||||
|
languageServer,
|
||||||
|
bufferRegistry,
|
||||||
|
capabilityRouter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
val messageHandler =
|
val messageHandler =
|
||||||
system.actorOf(
|
system.actorOf(
|
||||||
@ -68,8 +78,6 @@ class WebSocketServer(
|
|||||||
)
|
)
|
||||||
clientActor ! ClientApi.WebConnect(messageHandler)
|
clientActor ! ClientApi.WebConnect(messageHandler)
|
||||||
|
|
||||||
languageServer ! LanguageProtocol.Connect(clientId, clientActor)
|
|
||||||
|
|
||||||
val incomingMessages: Sink[Message, NotUsed] =
|
val incomingMessages: Sink[Message, NotUsed] =
|
||||||
Flow[Message]
|
Flow[Message]
|
||||||
.mapConcat({
|
.mapConcat({
|
||||||
|
@ -0,0 +1,44 @@
|
|||||||
|
package org.enso.languageserver.capability
|
||||||
|
|
||||||
|
import org.enso.languageserver.data.CapabilityRegistration
|
||||||
|
import org.enso.languageserver.jsonrpc.{HasParams, HasResult, Method, Unused}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The capability 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 CapabilityApi {
|
||||||
|
|
||||||
|
case object AcquireCapability extends Method("capability/acquire") {
|
||||||
|
implicit val hasParams = new HasParams[this.type] {
|
||||||
|
type Params = CapabilityRegistration
|
||||||
|
}
|
||||||
|
implicit val hasResult = new HasResult[this.type] {
|
||||||
|
type Result = Unused.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case object ReleaseCapability extends Method("capability/release") {
|
||||||
|
implicit val hasParams = new HasParams[this.type] {
|
||||||
|
type Params = CapabilityRegistration
|
||||||
|
}
|
||||||
|
implicit val hasResult = new HasResult[this.type] {
|
||||||
|
type Result = Unused.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case object ForceReleaseCapability
|
||||||
|
extends Method("capability/forceReleased") {
|
||||||
|
implicit val hasParams = new HasParams[this.type] {
|
||||||
|
type Params = CapabilityRegistration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case object GrantCapability extends Method("capability/granted") {
|
||||||
|
implicit val hasParams = new HasParams[this.type] {
|
||||||
|
type Params = CapabilityRegistration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,75 @@
|
|||||||
|
package org.enso.languageserver.capability
|
||||||
|
|
||||||
|
import org.enso.languageserver.data.{CapabilityRegistration, Client}
|
||||||
|
|
||||||
|
object CapabilityProtocol {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the Language Server grant a new capability to a client.
|
||||||
|
*
|
||||||
|
* @param client the client to grant the capability to.
|
||||||
|
* @param registration the capability to grant.
|
||||||
|
*/
|
||||||
|
case class AcquireCapability(
|
||||||
|
client: Client,
|
||||||
|
registration: CapabilityRegistration
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals capability acquisition status.
|
||||||
|
*/
|
||||||
|
sealed trait AcquireCapabilityResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms client that capability has been acquired.
|
||||||
|
*/
|
||||||
|
case object CapabilityAcquired extends AcquireCapabilityResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals that capability acquisition request cannot be processed.
|
||||||
|
*/
|
||||||
|
case object CapabilityAcquisitionBadRequest extends AcquireCapabilityResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the Language Server about a client releasing a capability.
|
||||||
|
*
|
||||||
|
* @param clientId the client releasing the capability.
|
||||||
|
* @param capability the capability being released.
|
||||||
|
*/
|
||||||
|
case class ReleaseCapability(
|
||||||
|
clientId: Client.Id,
|
||||||
|
capability: CapabilityRegistration
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals capability release status.
|
||||||
|
*/
|
||||||
|
sealed trait ReleaseCapabilityResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms client that capability has been released.
|
||||||
|
*/
|
||||||
|
case object CapabilityReleased extends ReleaseCapabilityResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals that capability release request cannot be processed.
|
||||||
|
*/
|
||||||
|
case object CapabilityReleaseBadRequest extends ReleaseCapabilityResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A notification sent by the Language Server, notifying a client about
|
||||||
|
* a capability being taken away from them.
|
||||||
|
*
|
||||||
|
* @param capability the capability being released.
|
||||||
|
*/
|
||||||
|
case class CapabilityForceReleased(capability: CapabilityRegistration)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A notification sent by the Language Server, notifying a client about a new
|
||||||
|
* capability being granted to them.
|
||||||
|
*
|
||||||
|
* @param registration the capability being granted.
|
||||||
|
*/
|
||||||
|
case class CapabilityGranted(registration: CapabilityRegistration)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,39 @@
|
|||||||
|
package org.enso.languageserver.capability
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorRef, Props}
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||||
|
AcquireCapability,
|
||||||
|
ReleaseCapability
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.data.{CanEdit, CapabilityRegistration}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A content based router that routes each capability request to the
|
||||||
|
* correct recipient based on the capability object.
|
||||||
|
*
|
||||||
|
* @param bufferRegistry the recipient of buffer capability requests
|
||||||
|
*/
|
||||||
|
class CapabilityRouter(bufferRegistry: ActorRef) extends Actor {
|
||||||
|
|
||||||
|
override def receive: Receive = {
|
||||||
|
case msg @ AcquireCapability(_, CapabilityRegistration(CanEdit(_))) =>
|
||||||
|
bufferRegistry.forward(msg)
|
||||||
|
|
||||||
|
case msg @ ReleaseCapability(_, CapabilityRegistration(CanEdit(_))) =>
|
||||||
|
bufferRegistry.forward(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object CapabilityRouter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a configuration object used to create a [[CapabilityRouter]]
|
||||||
|
*
|
||||||
|
* @param bufferRegistry a buffer registry ref
|
||||||
|
* @return a configuration object
|
||||||
|
*/
|
||||||
|
def props(bufferRegistry: ActorRef): Props =
|
||||||
|
Props(new CapabilityRouter(bufferRegistry))
|
||||||
|
|
||||||
|
}
|
@ -2,6 +2,7 @@ package org.enso.languageserver.data
|
|||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
import io.circe._
|
import io.circe._
|
||||||
|
import org.enso.languageserver.filemanager.Path
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A superclass for all capabilities in the system.
|
* A superclass for all capabilities in the system.
|
||||||
@ -12,9 +13,9 @@ sealed abstract class Capability(val method: String)
|
|||||||
//TODO[MK]: Migrate to actual Path, once it is implemented.
|
//TODO[MK]: Migrate to actual Path, once it is implemented.
|
||||||
/**
|
/**
|
||||||
* A capability allowing the user to modify a given file.
|
* A capability allowing the user to modify a given file.
|
||||||
* @param path
|
* @param path the file path this capability is granted for.
|
||||||
*/
|
*/
|
||||||
case class CanEdit(path: String) extends Capability(CanEdit.methodName)
|
case class CanEdit(path: Path) extends Capability(CanEdit.methodName)
|
||||||
|
|
||||||
object CanEdit {
|
object CanEdit {
|
||||||
val methodName = "canEdit"
|
val methodName = "canEdit"
|
||||||
@ -35,11 +36,9 @@ object Capability {
|
|||||||
/**
|
/**
|
||||||
* A capability registration object, used to identify acquired capabilities.
|
* A capability registration object, used to identify acquired capabilities.
|
||||||
*
|
*
|
||||||
* @param id the registration id.
|
|
||||||
* @param capability the registered capability.
|
* @param capability the registered capability.
|
||||||
*/
|
*/
|
||||||
case class CapabilityRegistration(
|
case class CapabilityRegistration(
|
||||||
id: CapabilityRegistration.Id,
|
|
||||||
capability: Capability
|
capability: Capability
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -49,13 +48,11 @@ object CapabilityRegistration {
|
|||||||
|
|
||||||
type Id = UUID
|
type Id = UUID
|
||||||
|
|
||||||
private val idField = "id"
|
|
||||||
private val methodField = "method"
|
private val methodField = "method"
|
||||||
private val optionsField = "registerOptions"
|
private val optionsField = "registerOptions"
|
||||||
|
|
||||||
implicit val encoder: Encoder[CapabilityRegistration] = registration =>
|
implicit val encoder: Encoder[CapabilityRegistration] = registration =>
|
||||||
Json.obj(
|
Json.obj(
|
||||||
idField -> registration.id.asJson,
|
|
||||||
methodField -> registration.capability.method.asJson,
|
methodField -> registration.capability.method.asJson,
|
||||||
optionsField -> registration.capability.asJson
|
optionsField -> registration.capability.asJson
|
||||||
)
|
)
|
||||||
@ -71,12 +68,11 @@ object CapabilityRegistration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
id <- json.downField(idField).as[Id]
|
|
||||||
method <- json.downField(methodField).as[String]
|
method <- json.downField(methodField).as[String]
|
||||||
capability <- resolveOptions(
|
capability <- resolveOptions(
|
||||||
method,
|
method,
|
||||||
json.downField(optionsField).focus.getOrElse(Json.Null)
|
json.downField(optionsField).focus.getOrElse(Json.Null)
|
||||||
)
|
)
|
||||||
} yield CapabilityRegistration(id, capability)
|
} yield CapabilityRegistration(capability)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,16 +8,14 @@ import akka.actor.ActorRef
|
|||||||
* @param id the internal id of this client
|
* @param id the internal id of this client
|
||||||
* @param actor the actor handling remote client communications, used to push
|
* @param actor the actor handling remote client communications, used to push
|
||||||
* requests and notifications.
|
* requests and notifications.
|
||||||
* @param capabilities the capabilities this client has available.
|
|
||||||
*/
|
*/
|
||||||
case class Client(
|
case class Client(
|
||||||
id: Client.Id,
|
id: Client.Id,
|
||||||
actor: ActorRef,
|
actor: ActorRef
|
||||||
capabilities: List[CapabilityRegistration]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
object Client {
|
object Client {
|
||||||
|
|
||||||
type Id = UUID
|
type Id = UUID
|
||||||
|
|
||||||
def apply(id: Id, actor: ActorRef): Client = Client(id, actor, List())
|
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
package org.enso.languageserver.data
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A content-based versioning calculator.
|
||||||
|
*/
|
||||||
|
trait ContentBasedVersioning {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates content-based version of document.
|
||||||
|
*
|
||||||
|
* @param content a textual content
|
||||||
|
* @return a digest
|
||||||
|
*/
|
||||||
|
def evalVersion(content: String): String
|
||||||
|
|
||||||
|
}
|
@ -48,79 +48,6 @@ case class Environment(clients: List[Client]) {
|
|||||||
def removeClient(clientId: Client.Id): Environment =
|
def removeClient(clientId: Client.Id): Environment =
|
||||||
copy(clients = clients.filter(_.id != clientId))
|
copy(clients = clients.filter(_.id != clientId))
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes all registered capabilities matching a given predicate.
|
|
||||||
*
|
|
||||||
* @param predicate the predicate to match capabilities against.
|
|
||||||
* @return a new version of `Env` without the capabilities matching the
|
|
||||||
* predicate and a list of all clients, together with capabilities
|
|
||||||
* that got removed for them.
|
|
||||||
*/
|
|
||||||
def removeCapabilitiesBy(
|
|
||||||
predicate: CapabilityRegistration => Boolean
|
|
||||||
): (Environment, List[(Client, List[CapabilityRegistration])]) = {
|
|
||||||
val newClients = clients.map { client =>
|
|
||||||
val (removedCapabilities, retainedCapabilities) =
|
|
||||||
client.capabilities.partition(predicate)
|
|
||||||
val newClient = client.copy(capabilities = retainedCapabilities)
|
|
||||||
(newClient, removedCapabilities)
|
|
||||||
}
|
|
||||||
(copy(clients = newClients.map(_._1)), newClients)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modified a client at a given id.
|
|
||||||
*
|
|
||||||
* @param clientId the id of the client to modify.
|
|
||||||
* @param modification the function used to modify the client.
|
|
||||||
* @return a new version of this env, with the selected client modified by
|
|
||||||
* `modification`
|
|
||||||
*/
|
|
||||||
def modifyClient(
|
|
||||||
clientId: Client.Id,
|
|
||||||
modification: Client => Client
|
|
||||||
): Environment = {
|
|
||||||
val newClients = clients.map { client =>
|
|
||||||
if (client.id == clientId) {
|
|
||||||
modification(client)
|
|
||||||
} else {
|
|
||||||
client
|
|
||||||
}
|
|
||||||
}
|
|
||||||
copy(clients = newClients)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grants a given client a provided capability.
|
|
||||||
*
|
|
||||||
* @param clientId the id of the client to grant the capability.
|
|
||||||
* @param registration the capability to grant.
|
|
||||||
* @return a new version of this env, with the capability granted.
|
|
||||||
*/
|
|
||||||
def grantCapability(
|
|
||||||
clientId: Client.Id,
|
|
||||||
registration: CapabilityRegistration
|
|
||||||
): Environment =
|
|
||||||
modifyClient(clientId, { client =>
|
|
||||||
client.copy(capabilities = registration :: client.capabilities)
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Releases a capability from a given client.
|
|
||||||
*
|
|
||||||
* @param clientId the id of the client that releases the capability.
|
|
||||||
* @param capabilityId the id of the capability registration to release.
|
|
||||||
* @return a new version of this env, with the selected capability released.
|
|
||||||
*/
|
|
||||||
def releaseCapability(
|
|
||||||
clientId: Client.Id,
|
|
||||||
capabilityId: CapabilityRegistration.Id
|
|
||||||
): Environment =
|
|
||||||
modifyClient(clientId, { client =>
|
|
||||||
client.copy(
|
|
||||||
capabilities = client.capabilities.filter(_.id != capabilityId)
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
object Environment {
|
object Environment {
|
||||||
|
@ -0,0 +1,23 @@
|
|||||||
|
package org.enso.languageserver.data
|
||||||
|
|
||||||
|
import org.bouncycastle.jcajce.provider.digest.SHA3
|
||||||
|
import org.bouncycastle.util.encoders.Hex
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SHA3-224 digest calculator.
|
||||||
|
*/
|
||||||
|
object Sha3_224VersionCalculator extends ContentBasedVersioning {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Digests textual content.
|
||||||
|
*
|
||||||
|
* @param content a textual content
|
||||||
|
* @return a digest
|
||||||
|
*/
|
||||||
|
override def evalVersion(content: String): String = {
|
||||||
|
val digestSHA3 = new SHA3.Digest224()
|
||||||
|
val hash = digestSHA3.digest(content.getBytes("UTF-8"))
|
||||||
|
Hex.toHexString(hash)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,22 @@
|
|||||||
|
package org.enso.languageserver.event
|
||||||
|
|
||||||
|
import org.enso.languageserver.filemanager.Path
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base trait for all buffer events.
|
||||||
|
*/
|
||||||
|
sealed trait BufferEvent extends Event
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the Language Server when new file is opened for editing.
|
||||||
|
*
|
||||||
|
* @param path the path to a file
|
||||||
|
*/
|
||||||
|
case class FileOpened(path: Path) extends BufferEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the Language Server when a file is closed for editing.
|
||||||
|
*
|
||||||
|
* @param path the path to a file
|
||||||
|
*/
|
||||||
|
case class FileClosed(path: Path) extends BufferEvent
|
@ -0,0 +1,23 @@
|
|||||||
|
package org.enso.languageserver.event
|
||||||
|
|
||||||
|
import org.enso.languageserver.data.Client
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base trait for all client events.
|
||||||
|
*/
|
||||||
|
sealed trait ClientEvent extends Event
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the Language Server about a new client connecting.
|
||||||
|
*
|
||||||
|
* @param client an object representing a client
|
||||||
|
*/
|
||||||
|
case class ClientConnected(client: Client) extends ClientEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies the Language Server about a client disconnecting.
|
||||||
|
* The client may not send any further messages after this one.
|
||||||
|
*
|
||||||
|
* @param clientId the internal id of this client
|
||||||
|
*/
|
||||||
|
case class ClientDisconnected(clientId: Client.Id) extends ClientEvent
|
@ -0,0 +1,6 @@
|
|||||||
|
package org.enso.languageserver.event
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Base trait for all server events.
|
||||||
|
*/
|
||||||
|
trait Event
|
@ -91,4 +91,6 @@ object FileManagerApi {
|
|||||||
|
|
||||||
case object FileExistsError extends Error(1004, "File already exists")
|
case object FileExistsError extends Error(1004, "File already exists")
|
||||||
|
|
||||||
|
case object OperationTimeoutError extends Error(1005, "IO operation timeout")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,11 @@ case object FileNotFound extends FileSystemFailure
|
|||||||
*/
|
*/
|
||||||
case object FileExists extends FileSystemFailure
|
case object FileExists extends FileSystemFailure
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signal that the operation timed out.
|
||||||
|
*/
|
||||||
|
case object OperationTimeout extends FileSystemFailure
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signals file system specific errors.
|
* Signals file system specific errors.
|
||||||
*
|
*
|
||||||
|
@ -5,7 +5,8 @@ import org.enso.languageserver.filemanager.FileManagerApi.{
|
|||||||
ContentRootNotFoundError,
|
ContentRootNotFoundError,
|
||||||
FileExistsError,
|
FileExistsError,
|
||||||
FileNotFoundError,
|
FileNotFoundError,
|
||||||
FileSystemError
|
FileSystemError,
|
||||||
|
OperationTimeoutError
|
||||||
}
|
}
|
||||||
import org.enso.languageserver.jsonrpc.Error
|
import org.enso.languageserver.jsonrpc.Error
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ object FileSystemFailureMapper {
|
|||||||
case AccessDenied => AccessDeniedError
|
case AccessDenied => AccessDeniedError
|
||||||
case FileNotFound => FileNotFoundError
|
case FileNotFound => FileNotFoundError
|
||||||
case FileExists => FileExistsError
|
case FileExists => FileExistsError
|
||||||
|
case OperationTimeout => OperationTimeoutError
|
||||||
case GenericFileSystemFailure(reason) => FileSystemError(reason)
|
case GenericFileSystemFailure(reason) => FileSystemError(reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,83 @@
|
|||||||
|
package org.enso.languageserver.requesthandler
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||||
|
import org.enso.languageserver.capability.CapabilityApi.AcquireCapability
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||||
|
CapabilityAcquired,
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request handler for `capability/acquire` commands.
|
||||||
|
*
|
||||||
|
* @param capabilityRouter a router that dispatches capability requests
|
||||||
|
* @param timeout a request timeout
|
||||||
|
* @param client an object representing a client connected to the language server
|
||||||
|
*/
|
||||||
|
class AcquireCapabilityHandler(
|
||||||
|
capabilityRouter: ActorRef,
|
||||||
|
timeout: FiniteDuration,
|
||||||
|
client: Client
|
||||||
|
) extends Actor
|
||||||
|
with ActorLogging {
|
||||||
|
|
||||||
|
import context.dispatcher
|
||||||
|
|
||||||
|
override def receive: Receive = requestStage
|
||||||
|
|
||||||
|
private def requestStage: Receive = {
|
||||||
|
case Request(AcquireCapability, id, registration: CapabilityRegistration) =>
|
||||||
|
capabilityRouter ! CapabilityProtocol.AcquireCapability(
|
||||||
|
client,
|
||||||
|
registration
|
||||||
|
)
|
||||||
|
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
|
||||||
|
context.become(responseStage(id, sender()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def responseStage(id: Id, replyTo: ActorRef): Receive = {
|
||||||
|
case RequestTimeout =>
|
||||||
|
log.error(s"Acquiring capability for ${client.id} timed out")
|
||||||
|
replyTo ! ResponseError(Some(id), ServiceError)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case CapabilityAcquired =>
|
||||||
|
replyTo ! ResponseResult(AcquireCapability, id, Unused)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case CapabilityAcquisitionBadRequest =>
|
||||||
|
replyTo ! ResponseError(Some(id), ServiceError)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def unhandled(message: Any): Unit =
|
||||||
|
log.warning("Received unknown message: {}", message)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object AcquireCapabilityHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a configuration object used to create a [[AcquireCapabilityHandler]]
|
||||||
|
*
|
||||||
|
* @param capabilityRouter a router that dispatches capability requests
|
||||||
|
* @param requestTimeout a request timeout
|
||||||
|
* @param client an object representing a client connected to the language server
|
||||||
|
* @return a configuration object
|
||||||
|
*/
|
||||||
|
def props(
|
||||||
|
capabilityRouter: ActorRef,
|
||||||
|
requestTimeout: FiniteDuration,
|
||||||
|
client: Client
|
||||||
|
): Props =
|
||||||
|
Props(
|
||||||
|
new AcquireCapabilityHandler(capabilityRouter, requestTimeout, client)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,91 @@
|
|||||||
|
package org.enso.languageserver.requesthandler
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||||
|
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.{
|
||||||
|
OpenFileResponse,
|
||||||
|
OpenFileResult
|
||||||
|
}
|
||||||
|
|
||||||
|
import scala.concurrent.duration.FiniteDuration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request handler for `text/openFile` commands.
|
||||||
|
*
|
||||||
|
* @param bufferRegistry a router that dispatches text editing requests
|
||||||
|
* @param timeout a request timeout
|
||||||
|
* @param client an object representing a client connected to the language server
|
||||||
|
*/
|
||||||
|
class OpenFileHandler(
|
||||||
|
bufferRegistry: ActorRef,
|
||||||
|
timeout: FiniteDuration,
|
||||||
|
client: Client
|
||||||
|
) extends Actor
|
||||||
|
with ActorLogging {
|
||||||
|
|
||||||
|
import context.dispatcher
|
||||||
|
|
||||||
|
override def receive: Receive = requestStage
|
||||||
|
|
||||||
|
private def requestStage: Receive = {
|
||||||
|
case Request(OpenFile, id, params: OpenFile.Params) =>
|
||||||
|
bufferRegistry ! TextProtocol.OpenFile(client, params.path)
|
||||||
|
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
|
||||||
|
context.become(responseStage(id, sender()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def responseStage(id: Id, replyTo: ActorRef): Receive = {
|
||||||
|
case RequestTimeout =>
|
||||||
|
log.error(s"Opening file for ${client.id} timed out")
|
||||||
|
replyTo ! ResponseError(Some(id), ServiceError)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case OpenFileResponse(Right(OpenFileResult(buffer, capability))) =>
|
||||||
|
replyTo ! ResponseResult(
|
||||||
|
OpenFile,
|
||||||
|
id,
|
||||||
|
OpenFile
|
||||||
|
.Result(capability, buffer.contents.toString, buffer.version)
|
||||||
|
)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case OpenFileResponse(Left(failure)) =>
|
||||||
|
replyTo ! ResponseError(
|
||||||
|
Some(id),
|
||||||
|
FileSystemFailureMapper.mapFailure(failure)
|
||||||
|
)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def unhandled(message: Any): Unit =
|
||||||
|
log.warning("Received unknown message: {}", message)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object OpenFileHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a configuration object used to create a [[OpenFileHandler]]
|
||||||
|
*
|
||||||
|
* @param bufferRegistry a router that dispatches text editing requests
|
||||||
|
* @param requestTimeout a request timeout
|
||||||
|
* @param client an object representing a client connected to the language server
|
||||||
|
* @return a configuration object
|
||||||
|
*/
|
||||||
|
def props(
|
||||||
|
bufferRegistry: ActorRef,
|
||||||
|
requestTimeout: FiniteDuration,
|
||||||
|
client: Client
|
||||||
|
): Props = Props(new OpenFileHandler(bufferRegistry, requestTimeout, client))
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package org.enso.languageserver.requesthandler
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorLogging, ActorRef, Props}
|
||||||
|
import org.enso.languageserver.capability.CapabilityApi.ReleaseCapability
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||||
|
CapabilityReleaseBadRequest,
|
||||||
|
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
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A request handler for `capability/release` commands.
|
||||||
|
*
|
||||||
|
* @param capabilityRouter a router that dispatches capability requests
|
||||||
|
* @param timeout a request timeout
|
||||||
|
* @param client an object representing a client connected to the language server
|
||||||
|
*/
|
||||||
|
class ReleaseCapabilityHandler(
|
||||||
|
capabilityRouter: ActorRef,
|
||||||
|
timeout: FiniteDuration,
|
||||||
|
client: Client
|
||||||
|
) extends Actor
|
||||||
|
with ActorLogging {
|
||||||
|
override def receive: Receive = requestStage
|
||||||
|
|
||||||
|
import context.dispatcher
|
||||||
|
|
||||||
|
private def requestStage: Receive = {
|
||||||
|
case Request(ReleaseCapability, id, params: CapabilityRegistration) =>
|
||||||
|
capabilityRouter ! CapabilityProtocol.ReleaseCapability(client.id, params)
|
||||||
|
context.system.scheduler.scheduleOnce(timeout, self, RequestTimeout)
|
||||||
|
context.become(responseStage(id, sender()))
|
||||||
|
}
|
||||||
|
private def responseStage(id: Id, replyTo: ActorRef): Receive = {
|
||||||
|
case RequestTimeout =>
|
||||||
|
log.error(s"Releasing capability for ${client.id} timed out")
|
||||||
|
replyTo ! ResponseError(Some(id), ServiceError)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case CapabilityReleased =>
|
||||||
|
replyTo ! ResponseResult(ReleaseCapability, id, Unused)
|
||||||
|
context.stop(self)
|
||||||
|
|
||||||
|
case CapabilityReleaseBadRequest =>
|
||||||
|
replyTo ! ResponseError(Some(id), ServiceError)
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def unhandled(message: Any): Unit =
|
||||||
|
log.warning("Received unknown message: {}", message)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object ReleaseCapabilityHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a configuration object used to create a [[ReleaseCapabilityHandler]]
|
||||||
|
*
|
||||||
|
* @param capabilityRouter a router that dispatches capability requests
|
||||||
|
* @param requestTimeout a request timeout
|
||||||
|
* @param client an object representing a client connected to the language server
|
||||||
|
* @return a configuration object
|
||||||
|
*/
|
||||||
|
def props(
|
||||||
|
capabilityRouter: ActorRef,
|
||||||
|
requestTimeout: FiniteDuration,
|
||||||
|
client: Client
|
||||||
|
): Props =
|
||||||
|
Props(
|
||||||
|
new ReleaseCapabilityHandler(capabilityRouter, requestTimeout, client)
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,6 @@
|
|||||||
|
package org.enso.languageserver.requesthandler
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Signals that operation has timed out.
|
||||||
|
*/
|
||||||
|
case object RequestTimeout
|
@ -0,0 +1,40 @@
|
|||||||
|
package org.enso.languageserver.text
|
||||||
|
|
||||||
|
import org.enso.languageserver.data.ContentBasedVersioning
|
||||||
|
import org.enso.languageserver.data.buffer.Rope
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A buffer state representation.
|
||||||
|
*
|
||||||
|
* @param contents the contents of the buffer.
|
||||||
|
* @param version the current version of the buffer contents.
|
||||||
|
*/
|
||||||
|
case class Buffer(contents: Rope, version: Buffer.Version)
|
||||||
|
|
||||||
|
object Buffer {
|
||||||
|
type Version = String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new buffer with a freshly generated version.
|
||||||
|
*
|
||||||
|
* @param contents the contents of this buffer.
|
||||||
|
* @param versionCalculator a digest calculator for content based versioning.
|
||||||
|
* @return a new buffer instance.
|
||||||
|
*/
|
||||||
|
def apply(
|
||||||
|
contents: Rope
|
||||||
|
)(implicit versionCalculator: ContentBasedVersioning): Buffer =
|
||||||
|
Buffer(contents, versionCalculator.evalVersion(contents.toString))
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new buffer with a freshly generated version.
|
||||||
|
*
|
||||||
|
* @param contents the contents of this buffer.
|
||||||
|
* @param versionCalculator a digest calculator for content based versioning.
|
||||||
|
* @return a new buffer instance.
|
||||||
|
*/
|
||||||
|
def apply(
|
||||||
|
contents: String
|
||||||
|
)(implicit versionCalculator: ContentBasedVersioning): Buffer =
|
||||||
|
Buffer(Rope(contents))
|
||||||
|
}
|
@ -0,0 +1,77 @@
|
|||||||
|
package org.enso.languageserver.text
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorRef, Props, Terminated}
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol.{
|
||||||
|
AcquireCapability,
|
||||||
|
CapabilityAcquisitionBadRequest,
|
||||||
|
CapabilityReleaseBadRequest,
|
||||||
|
ReleaseCapability
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.data.{
|
||||||
|
CanEdit,
|
||||||
|
CapabilityRegistration,
|
||||||
|
ContentBasedVersioning
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.filemanager.Path
|
||||||
|
import org.enso.languageserver.text.TextProtocol.OpenFile
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An actor that routes request regarding text editing to the right buffer.
|
||||||
|
* It creates a buffer actor, if a buffer doesn't exists.
|
||||||
|
*
|
||||||
|
* @param fileManager a file manager
|
||||||
|
* @param versionCalculator a content based version calculator
|
||||||
|
*/
|
||||||
|
class BufferRegistry(fileManager: ActorRef)(
|
||||||
|
implicit versionCalculator: ContentBasedVersioning
|
||||||
|
) extends Actor {
|
||||||
|
|
||||||
|
override def receive: Receive = running(Map.empty)
|
||||||
|
|
||||||
|
private def running(registry: Map[Path, ActorRef]): Receive = {
|
||||||
|
case msg @ OpenFile(_, path) =>
|
||||||
|
if (registry.contains(path)) {
|
||||||
|
registry(path).forward(msg)
|
||||||
|
} else {
|
||||||
|
val bufferRef =
|
||||||
|
context.actorOf(CollaborativeBuffer.props(path, fileManager))
|
||||||
|
context.watch(bufferRef)
|
||||||
|
bufferRef.forward(msg)
|
||||||
|
context.become(running(registry + (path -> bufferRef)))
|
||||||
|
}
|
||||||
|
|
||||||
|
case Terminated(bufferRef) =>
|
||||||
|
context.become(running(registry.filter(_._2 != bufferRef)))
|
||||||
|
|
||||||
|
case msg @ AcquireCapability(_, CapabilityRegistration(CanEdit(path))) =>
|
||||||
|
if (registry.contains(path)) {
|
||||||
|
registry(path).forward(msg)
|
||||||
|
} else {
|
||||||
|
sender() ! CapabilityAcquisitionBadRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
case msg @ ReleaseCapability(_, CapabilityRegistration(CanEdit(path))) =>
|
||||||
|
if (registry.contains(path)) {
|
||||||
|
registry(path).forward(msg)
|
||||||
|
} else {
|
||||||
|
sender() ! CapabilityReleaseBadRequest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object BufferRegistry {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a configuration object used to create a [[BufferRegistry]]
|
||||||
|
*
|
||||||
|
* @param fileManager a file manager actor
|
||||||
|
* @param versionCalculator a content based version calculator
|
||||||
|
* @return a configuration object
|
||||||
|
*/
|
||||||
|
def props(
|
||||||
|
fileManager: ActorRef
|
||||||
|
)(implicit versionCalculator: ContentBasedVersioning): Props =
|
||||||
|
Props(new BufferRegistry(fileManager))
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,237 @@
|
|||||||
|
package org.enso.languageserver.text
|
||||||
|
|
||||||
|
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
|
||||||
|
import org.enso.languageserver.capability.CapabilityProtocol._
|
||||||
|
import org.enso.languageserver.data.Client.Id
|
||||||
|
import org.enso.languageserver.data.{
|
||||||
|
CanEdit,
|
||||||
|
CapabilityRegistration,
|
||||||
|
Client,
|
||||||
|
ContentBasedVersioning
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.event.{
|
||||||
|
ClientDisconnected,
|
||||||
|
FileClosed,
|
||||||
|
FileOpened
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.filemanager.FileManagerProtocol.ReadFileResult
|
||||||
|
import org.enso.languageserver.filemanager.{
|
||||||
|
FileManagerProtocol,
|
||||||
|
OperationTimeout,
|
||||||
|
Path
|
||||||
|
}
|
||||||
|
import org.enso.languageserver.text.CollaborativeBuffer.FileReadingTimeout
|
||||||
|
import org.enso.languageserver.text.TextProtocol.{
|
||||||
|
OpenFile,
|
||||||
|
OpenFileResponse,
|
||||||
|
OpenFileResult
|
||||||
|
}
|
||||||
|
|
||||||
|
import scala.concurrent.duration._
|
||||||
|
import scala.language.postfixOps
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An actor enabling multiple users edit collaboratively a file.
|
||||||
|
*
|
||||||
|
* @param bufferPath a path to a file
|
||||||
|
* @param fileManager a file manger actor
|
||||||
|
* @param timeout a request timeout
|
||||||
|
* @param versionCalculator a content based version calculator
|
||||||
|
*/
|
||||||
|
class CollaborativeBuffer(
|
||||||
|
bufferPath: Path,
|
||||||
|
fileManager: ActorRef,
|
||||||
|
timeout: FiniteDuration
|
||||||
|
)(
|
||||||
|
implicit versionCalculator: ContentBasedVersioning
|
||||||
|
) extends Actor
|
||||||
|
with Stash
|
||||||
|
with ActorLogging {
|
||||||
|
|
||||||
|
import context.dispatcher
|
||||||
|
|
||||||
|
override def preStart(): Unit = {
|
||||||
|
context.system.eventStream.subscribe(self, classOf[ClientDisconnected])
|
||||||
|
}
|
||||||
|
|
||||||
|
override def receive: Receive = uninitialized
|
||||||
|
|
||||||
|
private def uninitialized: Receive = {
|
||||||
|
case OpenFile(client, path) =>
|
||||||
|
context.system.eventStream.publish(FileOpened(path))
|
||||||
|
log.info(s"Buffer opened for $path [client:${client.id}]")
|
||||||
|
readFile(client, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def waitingForFileContent(
|
||||||
|
client: Client,
|
||||||
|
replyTo: ActorRef
|
||||||
|
): Receive = {
|
||||||
|
case ReadFileResult(Right(content)) =>
|
||||||
|
handleFileContent(client, replyTo, content)
|
||||||
|
unstashAll()
|
||||||
|
|
||||||
|
case ReadFileResult(Left(failure)) =>
|
||||||
|
replyTo ! OpenFileResponse(Left(failure))
|
||||||
|
stop()
|
||||||
|
|
||||||
|
case FileReadingTimeout =>
|
||||||
|
replyTo ! OpenFileResponse(Left(OperationTimeout))
|
||||||
|
stop()
|
||||||
|
|
||||||
|
case _ => stash()
|
||||||
|
}
|
||||||
|
|
||||||
|
private def collaborativeEditing(
|
||||||
|
buffer: Buffer,
|
||||||
|
clients: Map[Client.Id, Client],
|
||||||
|
lockHolder: Option[Client]
|
||||||
|
): Receive = {
|
||||||
|
case OpenFile(client, _) =>
|
||||||
|
openFile(buffer, clients, lockHolder, client)
|
||||||
|
|
||||||
|
case AcquireCapability(clientId, CapabilityRegistration(CanEdit(path))) =>
|
||||||
|
acquireWriteLock(buffer, clients, lockHolder, clientId, path)
|
||||||
|
|
||||||
|
case ReleaseCapability(clientId, CapabilityRegistration(CanEdit(_))) =>
|
||||||
|
releaseWriteLock(buffer, clients, lockHolder, clientId)
|
||||||
|
|
||||||
|
case ClientDisconnected(clientId) =>
|
||||||
|
if (clients.contains(clientId)) {
|
||||||
|
removeClient(buffer, clients, lockHolder, clientId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private def readFile(client: Client, path: Path): Unit = {
|
||||||
|
fileManager ! FileManagerProtocol.ReadFile(path)
|
||||||
|
context.system.scheduler
|
||||||
|
.scheduleOnce(timeout, self, FileReadingTimeout)
|
||||||
|
context.become(waitingForFileContent(client, sender()))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def handleFileContent(
|
||||||
|
client: Client,
|
||||||
|
originalSender: ActorRef,
|
||||||
|
content: String
|
||||||
|
): Unit = {
|
||||||
|
val buffer = Buffer(content)
|
||||||
|
val cap = CapabilityRegistration(CanEdit(bufferPath))
|
||||||
|
originalSender ! OpenFileResponse(
|
||||||
|
Right(OpenFileResult(buffer, Some(cap)))
|
||||||
|
)
|
||||||
|
context.become(
|
||||||
|
collaborativeEditing(buffer, Map(client.id -> client), Some(client))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def openFile(
|
||||||
|
buffer: Buffer,
|
||||||
|
clients: Map[Id, Client],
|
||||||
|
lockHolder: Option[Client],
|
||||||
|
client: Client
|
||||||
|
): Unit = {
|
||||||
|
val writeCapability =
|
||||||
|
if (lockHolder.isEmpty)
|
||||||
|
Some(CapabilityRegistration(CanEdit(bufferPath)))
|
||||||
|
else
|
||||||
|
None
|
||||||
|
sender ! OpenFileResponse(Right(OpenFileResult(buffer, writeCapability)))
|
||||||
|
context.become(
|
||||||
|
collaborativeEditing(buffer, clients + (client.id -> client), lockHolder)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def removeClient(
|
||||||
|
buffer: Buffer,
|
||||||
|
clients: Map[Id, Client],
|
||||||
|
lockHolder: Option[Client],
|
||||||
|
clientId: Id
|
||||||
|
): Unit = {
|
||||||
|
val newLock =
|
||||||
|
lockHolder.flatMap {
|
||||||
|
case holder if (holder.id == clientId) => None
|
||||||
|
case holder => Some(holder)
|
||||||
|
}
|
||||||
|
val newClientMap = clients - clientId
|
||||||
|
if (newClientMap.isEmpty) {
|
||||||
|
stop()
|
||||||
|
} else {
|
||||||
|
context.become(collaborativeEditing(buffer, newClientMap, newLock))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def releaseWriteLock(
|
||||||
|
buffer: Buffer,
|
||||||
|
clients: Map[Client.Id, Client],
|
||||||
|
lockHolder: Option[Client],
|
||||||
|
clientId: Id
|
||||||
|
): Unit = {
|
||||||
|
lockHolder match {
|
||||||
|
case None =>
|
||||||
|
sender() ! CapabilityReleaseBadRequest
|
||||||
|
context.become(collaborativeEditing(buffer, clients, lockHolder))
|
||||||
|
|
||||||
|
case Some(holder) if holder.id != clientId =>
|
||||||
|
sender() ! CapabilityReleaseBadRequest
|
||||||
|
context.become(collaborativeEditing(buffer, clients, lockHolder))
|
||||||
|
|
||||||
|
case Some(holder) if holder.id == clientId =>
|
||||||
|
sender() ! CapabilityReleased
|
||||||
|
context.become(collaborativeEditing(buffer, clients, None))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def acquireWriteLock(
|
||||||
|
buffer: Buffer,
|
||||||
|
clients: Map[Client.Id, Client],
|
||||||
|
lockHolder: Option[Client],
|
||||||
|
clientId: Client,
|
||||||
|
path: Path
|
||||||
|
): Unit = {
|
||||||
|
lockHolder match {
|
||||||
|
case None =>
|
||||||
|
sender() ! CapabilityAcquired
|
||||||
|
context.become(collaborativeEditing(buffer, clients, Some(clientId)))
|
||||||
|
|
||||||
|
case Some(holder) if holder == clientId =>
|
||||||
|
sender() ! CapabilityAcquisitionBadRequest
|
||||||
|
context.become(collaborativeEditing(buffer, clients, lockHolder))
|
||||||
|
|
||||||
|
case Some(holder) if holder != clientId =>
|
||||||
|
sender() ! CapabilityAcquired
|
||||||
|
holder.actor ! CapabilityForceReleased(
|
||||||
|
CapabilityRegistration(CanEdit(path))
|
||||||
|
)
|
||||||
|
context.become(collaborativeEditing(buffer, clients, Some(clientId)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop(): Unit = {
|
||||||
|
context.system.eventStream.publish(FileClosed(bufferPath))
|
||||||
|
context.stop(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
object CollaborativeBuffer {
|
||||||
|
|
||||||
|
case object FileReadingTimeout
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a configuration object used to create a [[CollaborativeBuffer]]
|
||||||
|
*
|
||||||
|
* @param bufferPath a path to a file
|
||||||
|
* @param fileManager a file manager actor
|
||||||
|
* @param timeout a request timeout
|
||||||
|
* @param versionCalculator a content based version calculator
|
||||||
|
* @return a configuration object
|
||||||
|
*/
|
||||||
|
def props(
|
||||||
|
bufferPath: Path,
|
||||||
|
fileManager: ActorRef,
|
||||||
|
timeout: FiniteDuration = 10 seconds
|
||||||
|
)(implicit versionCalculator: ContentBasedVersioning): Props =
|
||||||
|
Props(new CollaborativeBuffer(bufferPath, fileManager, timeout))
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package org.enso.languageserver.text
|
||||||
|
|
||||||
|
import org.enso.languageserver.data.CapabilityRegistration
|
||||||
|
import org.enso.languageserver.filemanager.Path
|
||||||
|
import org.enso.languageserver.jsonrpc.{HasParams, HasResult, Method}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The text editing 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 TextApi {
|
||||||
|
|
||||||
|
case object OpenFile extends Method("text/openFile") {
|
||||||
|
case class Params(path: Path)
|
||||||
|
case class Result(
|
||||||
|
writeCapability: Option[CapabilityRegistration],
|
||||||
|
content: String,
|
||||||
|
currentVersion: String
|
||||||
|
)
|
||||||
|
implicit val hasParams = new HasParams[this.type] {
|
||||||
|
type Params = OpenFile.Params
|
||||||
|
}
|
||||||
|
implicit val hasResult = new HasResult[this.type] {
|
||||||
|
type Result = OpenFile.Result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package org.enso.languageserver.text
|
||||||
|
|
||||||
|
import org.enso.languageserver.data.{CapabilityRegistration, Client}
|
||||||
|
import org.enso.languageserver.filemanager.{FileSystemFailure, Path}
|
||||||
|
|
||||||
|
object TextProtocol {
|
||||||
|
|
||||||
|
/** Requests the language server to open a file on behalf of a given user.
|
||||||
|
*
|
||||||
|
* @param client the client opening the file.
|
||||||
|
* @param path the file path.
|
||||||
|
*/
|
||||||
|
case class OpenFile(client: Client, path: Path)
|
||||||
|
|
||||||
|
/** Sent by the server in response to [[OpenFile]]
|
||||||
|
*
|
||||||
|
* @param result either a file system failure, or successful opening data.
|
||||||
|
*/
|
||||||
|
case class OpenFileResponse(result: Either[FileSystemFailure, OpenFileResult])
|
||||||
|
|
||||||
|
/** The data carried by a successful file open operation.
|
||||||
|
*
|
||||||
|
* @param buffer file contents and current version.
|
||||||
|
* @param writeCapability a write capability that could have been
|
||||||
|
* automatically granted.
|
||||||
|
*/
|
||||||
|
case class OpenFileResult(
|
||||||
|
buffer: Buffer,
|
||||||
|
writeCapability: Option[CapabilityRegistration]
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
package org.enso.languageserver.data
|
||||||
|
|
||||||
|
import org.scalatest.flatspec.AnyFlatSpec
|
||||||
|
import org.scalatest.matchers.must.Matchers
|
||||||
|
|
||||||
|
class Sha3224VersionCalculatorSpec extends AnyFlatSpec with Matchers {
|
||||||
|
|
||||||
|
"A Sha3Digest" should "produce SHA3-224 digest" in {
|
||||||
|
Sha3_224VersionCalculator.evalVersion(" ") mustBe "4cb5f87b01b38adc0e6f13f915668c2394cb1fb7a2795635b894dda1"
|
||||||
|
Sha3_224VersionCalculator.evalVersion(
|
||||||
|
"The quick brown fox jumps over the lazy dog"
|
||||||
|
) mustBe "d15dadceaa4d5d7bb3b48f446421d542e08ad8887305e28d58335795"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package org.enso.languageserver.buffer
|
package org.enso.languageserver.text
|
||||||
import org.scalacheck.Arbitrary.arbitrary
|
import org.scalacheck.Arbitrary.arbitrary
|
||||||
import org.scalacheck.Gen
|
import org.scalacheck.Gen
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.enso.languageserver.buffer
|
package org.enso.languageserver.text
|
||||||
import org.enso.languageserver.data.buffer.StringUtils
|
import org.enso.languageserver.data.buffer.StringUtils
|
||||||
|
|
||||||
case class MockBuffer(lines: List[String]) {
|
case class MockBuffer(lines: List[String]) {
|
@ -1,4 +1,4 @@
|
|||||||
package org.enso.languageserver.buffer
|
package org.enso.languageserver.text
|
||||||
import org.enso.languageserver.data.buffer.Rope
|
import org.enso.languageserver.data.buffer.Rope
|
||||||
import org.scalacheck.Prop.forAll
|
import org.scalacheck.Prop.forAll
|
||||||
import org.scalacheck.Arbitrary._
|
import org.scalacheck.Arbitrary._
|
@ -1,4 +1,4 @@
|
|||||||
package org.enso.languageserver.buffer
|
package org.enso.languageserver.text
|
||||||
import org.enso.languageserver.data.buffer.StringUtils
|
import org.enso.languageserver.data.buffer.StringUtils
|
||||||
import org.scalacheck.Properties
|
import org.scalacheck.Properties
|
||||||
import org.scalacheck.Prop.forAll
|
import org.scalacheck.Prop.forAll
|
@ -1,174 +0,0 @@
|
|||||||
package org.enso.languageserver.websocket
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
import io.circe.literal._
|
|
||||||
|
|
||||||
class CapabilitiesTest extends WebSocketServerTest {
|
|
||||||
"Language Server" must {
|
|
||||||
"be able to grant and release capabilities" in {
|
|
||||||
val probe = new WsTestClient(address)
|
|
||||||
val capabilityId = UUID.randomUUID()
|
|
||||||
probe.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/acquire",
|
|
||||||
"id": 1,
|
|
||||||
"params": {
|
|
||||||
"id": $capabilityId,
|
|
||||||
"method": "canEdit",
|
|
||||||
"registerOptions": { "path": "Foo/bar" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
probe.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"result": null
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
probe.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/release",
|
|
||||||
"id": 2,
|
|
||||||
"params": {
|
|
||||||
"id": $capabilityId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
probe.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"id": 2,
|
|
||||||
"result": null
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
"take canEdit capability away from clients when another client registers for it" in {
|
|
||||||
val client1 = new WsTestClient(address)
|
|
||||||
val client2 = new WsTestClient(address)
|
|
||||||
val client3 = new WsTestClient(address)
|
|
||||||
val capability1Id = UUID.randomUUID()
|
|
||||||
val capability2Id = UUID.randomUUID()
|
|
||||||
val capability3Id = UUID.randomUUID()
|
|
||||||
|
|
||||||
client1.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/acquire",
|
|
||||||
"id": 1,
|
|
||||||
"params": {
|
|
||||||
"id": $capability1Id,
|
|
||||||
"method": "canEdit",
|
|
||||||
"registerOptions": { "path": "Foo/bar" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
client1.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"result": null
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
client2.expectNoMessage()
|
|
||||||
client3.expectNoMessage()
|
|
||||||
|
|
||||||
client2.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/acquire",
|
|
||||||
"id": 2,
|
|
||||||
"params": {
|
|
||||||
"id": $capability2Id,
|
|
||||||
"method": "canEdit",
|
|
||||||
"registerOptions": { "path": "Foo/bar" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
client1.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/forceReleased",
|
|
||||||
"params": {"id": $capability1Id}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
client2.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"id": 2,
|
|
||||||
"result": null
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
client3.expectNoMessage()
|
|
||||||
|
|
||||||
client3.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/acquire",
|
|
||||||
"id": 3,
|
|
||||||
"params": {
|
|
||||||
"id": $capability3Id,
|
|
||||||
"method": "canEdit",
|
|
||||||
"registerOptions": { "path": "Foo/bar" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
client1.expectNoMessage()
|
|
||||||
client2.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/forceReleased",
|
|
||||||
"params": {"id": $capability2Id}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
client3.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"id": 3,
|
|
||||||
"result": null
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
|
|
||||||
"implement the canEdit capability on a per-file basis" in {
|
|
||||||
val client1 = new WsTestClient(address)
|
|
||||||
val client2 = new WsTestClient(address)
|
|
||||||
val capability1Id = UUID.randomUUID()
|
|
||||||
val capability2Id = UUID.randomUUID()
|
|
||||||
|
|
||||||
client1.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/acquire",
|
|
||||||
"id": 1,
|
|
||||||
"params": {
|
|
||||||
"id": $capability1Id,
|
|
||||||
"method": "canEdit",
|
|
||||||
"registerOptions": { "path": "Foo/bar" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
client1.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"id": 1,
|
|
||||||
"result": null
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
client2.expectNoMessage()
|
|
||||||
|
|
||||||
client2.send(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"method": "capability/acquire",
|
|
||||||
"id": 2,
|
|
||||||
"params": {
|
|
||||||
"id": $capability2Id,
|
|
||||||
"method": "canEdit",
|
|
||||||
"registerOptions": { "path": "Foo/baz" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
|
|
||||||
client1.expectNoMessage()
|
|
||||||
client2.expectJson(json"""
|
|
||||||
{ "jsonrpc": "2.0",
|
|
||||||
"id": 2,
|
|
||||||
"result": null
|
|
||||||
}
|
|
||||||
""")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,459 @@
|
|||||||
|
package org.enso.languageserver.websocket
|
||||||
|
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
import io.circe.literal._
|
||||||
|
|
||||||
|
class TextOperationsTest extends WebSocketServerTest {
|
||||||
|
|
||||||
|
"text/openFile" must {
|
||||||
|
"fail opening a file if it does not exist" in {
|
||||||
|
// Interaction:
|
||||||
|
// 1. Client tries to open a non-existent file.
|
||||||
|
// 2. Client receives an error message.
|
||||||
|
val client = new WsTestClient(address)
|
||||||
|
|
||||||
|
// 1
|
||||||
|
client.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "text/openFile",
|
||||||
|
"id": 1,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 2
|
||||||
|
client.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"error": { "code": 1003, "message": "File not found" }
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
"allow opening files for editing" in {
|
||||||
|
// Interaction:
|
||||||
|
// 1. Client creates a file.
|
||||||
|
// 2. Client receives confirmation.
|
||||||
|
// 3. Client opens the created file.
|
||||||
|
// 4. Client receives the file contents and a canEdit capability.
|
||||||
|
val client = new WsTestClient(address)
|
||||||
|
|
||||||
|
// 1
|
||||||
|
client.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "file/write",
|
||||||
|
"id": 0,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
},
|
||||||
|
"contents": "123456789"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 2
|
||||||
|
client.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 0,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 3
|
||||||
|
client.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "text/openFile",
|
||||||
|
"id": 1,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 4
|
||||||
|
client.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"writeCapability": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": { "path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": ["foo.txt"]
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
"content": "123456789",
|
||||||
|
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
"allow opening files for reading for another client" in {
|
||||||
|
// Interaction:
|
||||||
|
// 1. Client 1 creates a file.
|
||||||
|
// 2. Client 1 receives confirmation.
|
||||||
|
// 3. Client 1 opens the created file.
|
||||||
|
// 4. Client 1 receives the file contents and a canEdit capability.
|
||||||
|
// 5. Client 2 opens the file.
|
||||||
|
// 6. Client 2 receives the file contents without a canEdit capability.
|
||||||
|
val client1 = new WsTestClient(address)
|
||||||
|
val client2 = new WsTestClient(address)
|
||||||
|
|
||||||
|
// 1
|
||||||
|
client1.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "file/write",
|
||||||
|
"id": 0,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
},
|
||||||
|
"contents": "123456789"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 2
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 0,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 3
|
||||||
|
client1.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "text/openFile",
|
||||||
|
"id": 1,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 4
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"writeCapability": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": { "path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": ["foo.txt"]
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
"content": "123456789",
|
||||||
|
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 5
|
||||||
|
client2.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "text/openFile",
|
||||||
|
"id": 2,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 6
|
||||||
|
client2.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"result": {
|
||||||
|
"writeCapability": null,
|
||||||
|
"content": "123456789",
|
||||||
|
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
"grant the canEdit capability if no one else holds it" in {
|
||||||
|
// Interaction:
|
||||||
|
// 1. Client 1 creates a file.
|
||||||
|
// 2. Client 1 receives confirmation.
|
||||||
|
// 3. Client 1 opens the created file.
|
||||||
|
// 4. Client 1 receives the file contents and a canEdit capability.
|
||||||
|
// 5. Client 1 releases the canEdit capability.
|
||||||
|
// 6. Client 1 receives a confirmation.
|
||||||
|
// 7. Client 2 opens the file.
|
||||||
|
// 8. Client 2 receives the file contents and a canEdit capability.
|
||||||
|
val client1 = new WsTestClient(address)
|
||||||
|
val client2 = new WsTestClient(address)
|
||||||
|
|
||||||
|
// 1
|
||||||
|
client1.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "file/write",
|
||||||
|
"id": 0,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
},
|
||||||
|
"contents": "123456789"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 2
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 0,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 3
|
||||||
|
client1.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "text/openFile",
|
||||||
|
"id": 1,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 4
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"writeCapability": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": { "path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": ["foo.txt"]
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
"content": "123456789",
|
||||||
|
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 5
|
||||||
|
client1.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "capability/release",
|
||||||
|
"id": 2,
|
||||||
|
"params": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": { "path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": ["foo.txt"]
|
||||||
|
} }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 6
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 7
|
||||||
|
client2.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "text/openFile",
|
||||||
|
"id": 3,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
// 8
|
||||||
|
client2.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 3,
|
||||||
|
"result": {
|
||||||
|
"writeCapability": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": { "path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": ["foo.txt"]
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
"content": "123456789",
|
||||||
|
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
"take canEdit capability away from clients when another client registers for it" in {
|
||||||
|
val client1 = new WsTestClient(address)
|
||||||
|
val client2 = new WsTestClient(address)
|
||||||
|
val client3 = new WsTestClient(address)
|
||||||
|
val capability1Id = UUID.randomUUID()
|
||||||
|
val capability2Id = UUID.randomUUID()
|
||||||
|
val capability3Id = UUID.randomUUID()
|
||||||
|
|
||||||
|
client1.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "file/write",
|
||||||
|
"id": 0,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
},
|
||||||
|
"contents": "123456789"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 0,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
client1.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "text/openFile",
|
||||||
|
"id": 1,
|
||||||
|
"params": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 1,
|
||||||
|
"result": {
|
||||||
|
"writeCapability": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": { "path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": ["foo.txt"]
|
||||||
|
} }
|
||||||
|
},
|
||||||
|
"content": "123456789",
|
||||||
|
"currentVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
client2.expectNoMessage()
|
||||||
|
client3.expectNoMessage()
|
||||||
|
|
||||||
|
client2.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "capability/acquire",
|
||||||
|
"id": 2,
|
||||||
|
"params": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
client1.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "capability/forceReleased",
|
||||||
|
"params": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
client2.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 2,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
client3.expectNoMessage()
|
||||||
|
|
||||||
|
client3.send(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "capability/acquire",
|
||||||
|
"id": 3,
|
||||||
|
"params": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
|
||||||
|
client1.expectNoMessage()
|
||||||
|
client2.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"method": "capability/forceReleased",
|
||||||
|
"params": {
|
||||||
|
"method": "canEdit",
|
||||||
|
"registerOptions": {
|
||||||
|
"path": {
|
||||||
|
"rootId": $testContentRootId,
|
||||||
|
"segments": [ "foo.txt" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
client3.expectJson(json"""
|
||||||
|
{ "jsonrpc": "2.0",
|
||||||
|
"id": 3,
|
||||||
|
"result": null
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -12,14 +12,15 @@ import akka.testkit.{ImplicitSender, TestKit, TestProbe}
|
|||||||
import cats.effect.IO
|
import cats.effect.IO
|
||||||
import io.circe.Json
|
import io.circe.Json
|
||||||
import io.circe.parser.parse
|
import io.circe.parser.parse
|
||||||
import org.enso.languageserver.data.Config
|
import org.enso.languageserver.capability.CapabilityRouter
|
||||||
|
import org.enso.languageserver.data.{Config, Sha3_224VersionCalculator}
|
||||||
import org.enso.languageserver.{
|
import org.enso.languageserver.{
|
||||||
LanguageProtocol,
|
LanguageProtocol,
|
||||||
LanguageServer,
|
LanguageServer,
|
||||||
WebSocketServer
|
WebSocketServer
|
||||||
}
|
}
|
||||||
import org.enso.languageserver.filemanager.FileSystem
|
import org.enso.languageserver.filemanager.FileSystem
|
||||||
|
import org.enso.languageserver.text.BufferRegistry
|
||||||
import org.scalatest.{Assertion, BeforeAndAfterAll, BeforeAndAfterEach}
|
import org.scalatest.{Assertion, BeforeAndAfterAll, BeforeAndAfterEach}
|
||||||
import org.scalatest.matchers.should.Matchers
|
import org.scalatest.matchers.should.Matchers
|
||||||
import org.scalatest.wordspec.AnyWordSpecLike
|
import org.scalatest.wordspec.AnyWordSpecLike
|
||||||
@ -57,7 +58,16 @@ abstract class WebSocketServerTest
|
|||||||
Props(new LanguageServer(config, new FileSystem[IO]))
|
Props(new LanguageServer(config, new FileSystem[IO]))
|
||||||
)
|
)
|
||||||
languageServer ! LanguageProtocol.Initialize
|
languageServer ! LanguageProtocol.Initialize
|
||||||
server = new WebSocketServer(languageServer)
|
val bufferRegistry =
|
||||||
|
system.actorOf(
|
||||||
|
BufferRegistry.props(languageServer)(Sha3_224VersionCalculator)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val capabilityRouter =
|
||||||
|
system.actorOf(CapabilityRouter.props(bufferRegistry))
|
||||||
|
|
||||||
|
server =
|
||||||
|
new WebSocketServer(languageServer, bufferRegistry, capabilityRouter)
|
||||||
binding = Await.result(server.bind(interface, port = 0), 3.seconds)
|
binding = Await.result(server.bind(interface, port = 0), 3.seconds)
|
||||||
address = s"ws://$interface:${binding.localAddress.getPort}"
|
address = s"ws://$interface:${binding.localAddress.getPort}"
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,8 @@ import org.enso.languageserver.filemanager.FileSystem
|
|||||||
import org.enso.languageserver.{
|
import org.enso.languageserver.{
|
||||||
LanguageProtocol,
|
LanguageProtocol,
|
||||||
LanguageServer,
|
LanguageServer,
|
||||||
|
LanguageServerConfig,
|
||||||
|
MainModule,
|
||||||
WebSocketServer
|
WebSocketServer
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -29,22 +31,16 @@ object LanguageServerApp {
|
|||||||
*/
|
*/
|
||||||
def run(config: LanguageServerConfig): Unit = {
|
def run(config: LanguageServerConfig): Unit = {
|
||||||
println("Starting Language Server...")
|
println("Starting Language Server...")
|
||||||
implicit val system = ActorSystem()
|
val mainModule = new MainModule(config)
|
||||||
implicit val materializer = SystemMaterializer.get(system)
|
|
||||||
val languageServerConfig = Config(
|
|
||||||
Map(config.contentRootUuid -> new File(config.contentRootPath))
|
|
||||||
)
|
|
||||||
val languageServer =
|
|
||||||
system.actorOf(
|
|
||||||
Props(new LanguageServer(languageServerConfig, new FileSystem[IO]))
|
|
||||||
)
|
|
||||||
|
|
||||||
languageServer ! LanguageProtocol.Initialize
|
mainModule.languageServer ! LanguageProtocol.Initialize
|
||||||
|
|
||||||
val server = new WebSocketServer(languageServer)
|
|
||||||
|
|
||||||
val binding =
|
val binding =
|
||||||
Await.result(server.bind(config.interface, config.port), 3.seconds)
|
Await.result(
|
||||||
|
mainModule.server.bind(config.interface, config.port),
|
||||||
|
3.seconds
|
||||||
|
)
|
||||||
|
|
||||||
println(
|
println(
|
||||||
s"Started server at ${config.interface}:${config.port}, press enter to kill server"
|
s"Started server at ${config.interface}:${config.port}, press enter to kill server"
|
||||||
)
|
)
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
package org.enso.runner
|
|
||||||
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
case class LanguageServerConfig(
|
|
||||||
interface: String,
|
|
||||||
port: Int,
|
|
||||||
contentRootUuid: UUID,
|
|
||||||
contentRootPath: String
|
|
||||||
)
|
|
@ -5,6 +5,8 @@ import java.util.UUID
|
|||||||
|
|
||||||
import cats.implicits._
|
import cats.implicits._
|
||||||
import org.apache.commons.cli.{Option => CliOption, _}
|
import org.apache.commons.cli.{Option => CliOption, _}
|
||||||
|
import org.enso.languageserver
|
||||||
|
import org.enso.languageserver.LanguageServerConfig
|
||||||
import org.enso.pkg.Package
|
import org.enso.pkg.Package
|
||||||
import org.enso.polyglot.{ExecutionContext, LanguageInfo, Module}
|
import org.enso.polyglot.{ExecutionContext, LanguageInfo, Module}
|
||||||
import org.graalvm.polyglot.Value
|
import org.graalvm.polyglot.Value
|
||||||
@ -244,7 +246,7 @@ object Main {
|
|||||||
port <- Either
|
port <- Either
|
||||||
.catchNonFatal(portString.toInt)
|
.catchNonFatal(portString.toInt)
|
||||||
.leftMap(_ => "Port must be integer")
|
.leftMap(_ => "Port must be integer")
|
||||||
} yield LanguageServerConfig(interface, port, rootId, rootPath)
|
} yield languageserver.LanguageServerConfig(interface, port, rootId, rootPath)
|
||||||
// format: on
|
// format: on
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Loading…
Reference in New Issue
Block a user