mirror of
https://github.com/enso-org/enso.git
synced 2024-12-24 03:02:30 +03:00
'text/save' method (#601)
This commit is contained in:
parent
c2df4e7957
commit
7a1b333f2c
@ -1631,7 +1631,14 @@ null
|
||||
```
|
||||
|
||||
##### Errors
|
||||
TBC
|
||||
- [`FileNotOpenedError`](#filenotopenederror) to signal that the file isn't
|
||||
open.
|
||||
- [`InvalidVersionError`](#invalidversionerror) to signal that the version provided by the client doesn't match the version
|
||||
computed by the server.
|
||||
- [`WriteDeniedError`](#writedeniederror) to signal that the client doesn't hold write lock for the buffer.
|
||||
- [`FileSystemError`](#filesystemerror) to signal a generic, unrecoverable file-system error.
|
||||
- [`ContentRootNotFoundError`](#contentrootnotfounderror) to signal that the requested content root cannot be found.
|
||||
- [`AccessDeniedError`](#accessdeniederror) to signal that the user doesn't have access to a resource.
|
||||
|
||||
#### `text/applyEdit`
|
||||
This requests that the server apply a series of edits to the project. These
|
||||
@ -1659,12 +1666,12 @@ null
|
||||
```
|
||||
|
||||
##### Errors
|
||||
- [`FileNotOpenedError`](#filenotopenederror) to signal that a file wasn't
|
||||
opened.
|
||||
- [`FileNotOpenedError`](#filenotopenederror) to signal that the file isn't
|
||||
open.
|
||||
- [`TextEditValidationError`](#texteditvalidationerror) to signal that validation has failed for a series of edits.
|
||||
- [`InvalidVersionError`](#invalidversionerror) to signal that version provided by a client doesn't match to the version
|
||||
- [`InvalidVersionError`](#invalidversionerror) to signal that the version provided by the client doesn't match the version
|
||||
computed by the server.
|
||||
- [`WriteDeniedError`](#writedeniederror) to signal that the client doesn't hold write lock to the buffer.
|
||||
- [`WriteDeniedError`](#writedeniederror) to signal that the client doesn't hold write lock for the buffer.
|
||||
|
||||
#### `text/didChange`
|
||||
This is a notification sent from the server to the clients to inform them of any
|
||||
|
@ -28,12 +28,14 @@ import org.enso.languageserver.requesthandler.{
|
||||
ApplyEditHandler,
|
||||
CloseFileHandler,
|
||||
OpenFileHandler,
|
||||
ReleaseCapabilityHandler
|
||||
ReleaseCapabilityHandler,
|
||||
SaveFileHandler
|
||||
}
|
||||
import org.enso.languageserver.text.TextApi.{
|
||||
ApplyEdit,
|
||||
CloseFile,
|
||||
OpenFile,
|
||||
SaveFile,
|
||||
TextDidChange
|
||||
}
|
||||
import org.enso.languageserver.text.TextProtocol
|
||||
@ -57,6 +59,7 @@ object ClientApi {
|
||||
.registerRequest(CreateFile)
|
||||
.registerRequest(OpenFile)
|
||||
.registerRequest(CloseFile)
|
||||
.registerRequest(SaveFile)
|
||||
.registerRequest(ApplyEdit)
|
||||
.registerRequest(DeleteFile)
|
||||
.registerRequest(CopyFile)
|
||||
@ -102,7 +105,8 @@ class ClientController(
|
||||
CloseFile -> CloseFileHandler
|
||||
.props(bufferRegistry, requestTimeout, client),
|
||||
ApplyEdit -> ApplyEditHandler
|
||||
.props(bufferRegistry, requestTimeout, client)
|
||||
.props(bufferRegistry, requestTimeout, client),
|
||||
SaveFile -> SaveFileHandler.props(bufferRegistry, requestTimeout, client)
|
||||
)
|
||||
|
||||
override def unhandled(message: Any): Unit =
|
||||
|
@ -15,7 +15,7 @@ import org.enso.languageserver.text.TextProtocol
|
||||
import org.enso.languageserver.text.TextProtocol.{
|
||||
ApplyEditSuccess,
|
||||
FileNotOpened,
|
||||
InvalidVersion,
|
||||
TextEditInvalidVersion,
|
||||
TextEditValidationFailed,
|
||||
WriteDenied
|
||||
}
|
||||
@ -61,7 +61,7 @@ class ApplyEditHandler(
|
||||
replyTo ! ResponseError(Some(id), TextEditValidationError(msg))
|
||||
context.stop(self)
|
||||
|
||||
case InvalidVersion(clientVersion, serverVersion) =>
|
||||
case TextEditInvalidVersion(clientVersion, serverVersion) =>
|
||||
replyTo ! ResponseError(
|
||||
Some(id),
|
||||
InvalidVersionError(clientVersion, serverVersion)
|
||||
|
@ -0,0 +1,108 @@
|
||||
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._
|
||||
import org.enso.languageserver.text.TextApi.{
|
||||
FileNotOpenedError,
|
||||
InvalidVersionError,
|
||||
SaveFile,
|
||||
WriteDeniedError
|
||||
}
|
||||
import org.enso.languageserver.text.TextProtocol
|
||||
import org.enso.languageserver.text.TextProtocol.{
|
||||
FileNotOpened,
|
||||
FileSaved,
|
||||
SaveDenied,
|
||||
SaveFailed,
|
||||
SaveFileInvalidVersion
|
||||
}
|
||||
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
|
||||
/**
|
||||
* A request handler for `text/save` 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 SaveFileHandler(
|
||||
bufferRegistry: ActorRef,
|
||||
timeout: FiniteDuration,
|
||||
client: Client
|
||||
) extends Actor
|
||||
with ActorLogging {
|
||||
|
||||
import context.dispatcher
|
||||
|
||||
override def receive: Receive = requestStage
|
||||
|
||||
private def requestStage: Receive = {
|
||||
case Request(SaveFile, id, params: SaveFile.Params) =>
|
||||
bufferRegistry ! TextProtocol.SaveFile(
|
||||
client.id,
|
||||
params.path,
|
||||
params.currentVersion
|
||||
)
|
||||
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"Saving file for ${client.id} timed out")
|
||||
replyTo ! ResponseError(Some(id), ServiceError)
|
||||
context.stop(self)
|
||||
|
||||
case FileSaved =>
|
||||
replyTo ! ResponseResult(SaveFile, id, Unused)
|
||||
context.stop(self)
|
||||
|
||||
case SaveFailed(fsFailure) =>
|
||||
replyTo ! ResponseError(
|
||||
Some(id),
|
||||
FileSystemFailureMapper.mapFailure(fsFailure)
|
||||
)
|
||||
context.stop(self)
|
||||
|
||||
case SaveDenied =>
|
||||
replyTo ! ResponseError(Some(id), WriteDeniedError)
|
||||
context.stop(self)
|
||||
|
||||
case SaveFileInvalidVersion(clientVersion, serverVersion) =>
|
||||
replyTo ! ResponseError(
|
||||
Some(id),
|
||||
InvalidVersionError(clientVersion, serverVersion)
|
||||
)
|
||||
context.stop(self)
|
||||
|
||||
case FileNotOpened =>
|
||||
replyTo ! ResponseError(Some(id), FileNotOpenedError)
|
||||
context.stop(self)
|
||||
}
|
||||
|
||||
override def unhandled(message: Any): Unit =
|
||||
log.warning("Received unknown message: {}", message)
|
||||
|
||||
}
|
||||
|
||||
object SaveFileHandler {
|
||||
|
||||
/**
|
||||
* Creates a configuration object used to create a [[SaveFileHandler]].
|
||||
*
|
||||
* @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 SaveFileHandler(bufferRegistry, requestTimeout, client))
|
||||
|
||||
}
|
@ -14,9 +14,11 @@ import org.enso.languageserver.data.{
|
||||
}
|
||||
import org.enso.languageserver.filemanager.Path
|
||||
import org.enso.languageserver.text.TextProtocol.{
|
||||
ApplyEdit,
|
||||
CloseFile,
|
||||
FileNotOpened,
|
||||
OpenFile
|
||||
OpenFile,
|
||||
SaveFile
|
||||
}
|
||||
import org.enso.languageserver.text.editing.model.FileEdit
|
||||
|
||||
@ -69,7 +71,14 @@ class BufferRegistry(fileManager: ActorRef)(
|
||||
sender() ! CapabilityReleaseBadRequest
|
||||
}
|
||||
|
||||
case msg @ TextProtocol.ApplyEdit(_, FileEdit(path, _, _, _)) =>
|
||||
case msg @ ApplyEdit(_, FileEdit(path, _, _, _)) =>
|
||||
if (registry.contains(path)) {
|
||||
registry(path).forward(msg)
|
||||
} else {
|
||||
sender() ! FileNotOpened
|
||||
}
|
||||
|
||||
case msg @ SaveFile(_, path, _) =>
|
||||
if (registry.contains(path)) {
|
||||
registry(path).forward(msg)
|
||||
} else {
|
||||
|
@ -1,9 +1,9 @@
|
||||
package org.enso.languageserver.text
|
||||
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Props, Stash}
|
||||
import akka.actor.{Actor, ActorLogging, ActorRef, Cancellable, Props, Stash}
|
||||
import cats.implicits._
|
||||
import org.enso.languageserver.capability.CapabilityProtocol._
|
||||
import org.enso.languageserver.data.Client.Id
|
||||
import org.enso.languageserver.data.buffer.Rope
|
||||
import org.enso.languageserver.data.{
|
||||
CanEdit,
|
||||
CapabilityRegistration,
|
||||
@ -15,23 +15,20 @@ import org.enso.languageserver.event.{
|
||||
BufferOpened,
|
||||
ClientDisconnected
|
||||
}
|
||||
import org.enso.languageserver.filemanager.FileManagerProtocol.ReadFileResult
|
||||
import org.enso.languageserver.filemanager.FileManagerProtocol.{
|
||||
ReadFileResult,
|
||||
WriteFileResult
|
||||
}
|
||||
import org.enso.languageserver.filemanager.{
|
||||
FileManagerProtocol,
|
||||
OperationTimeout,
|
||||
Path
|
||||
}
|
||||
import org.enso.languageserver.text.CollaborativeBuffer.FileReadingTimeout
|
||||
import org.enso.languageserver.text.Buffer.Version
|
||||
import org.enso.languageserver.text.CollaborativeBuffer.IOTimeout
|
||||
import org.enso.languageserver.text.TextProtocol._
|
||||
import org.enso.languageserver.text.editing.{
|
||||
EditorOps,
|
||||
EndPositionBeforeStartPosition,
|
||||
InvalidPosition,
|
||||
NegativeCoordinateInPosition,
|
||||
TextEditValidationFailure
|
||||
}
|
||||
import org.enso.languageserver.text.editing.model.{FileEdit, Position, TextEdit}
|
||||
import cats.implicits._
|
||||
import org.enso.languageserver.text.editing._
|
||||
import org.enso.languageserver.text.editing.model.{FileEdit, TextEdit}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.language.postfixOps
|
||||
@ -62,6 +59,9 @@ class CollaborativeBuffer(
|
||||
|
||||
override def receive: Receive = uninitialized
|
||||
|
||||
override def unhandled(message: Any): Unit =
|
||||
log.warning("Received unknown message: {}", message)
|
||||
|
||||
private def uninitialized: Receive = {
|
||||
case OpenFile(client, path) =>
|
||||
context.system.eventStream.publish(BufferOpened(path))
|
||||
@ -71,17 +71,20 @@ class CollaborativeBuffer(
|
||||
|
||||
private def waitingForFileContent(
|
||||
client: Client,
|
||||
replyTo: ActorRef
|
||||
replyTo: ActorRef,
|
||||
timeoutCancellable: Cancellable
|
||||
): Receive = {
|
||||
case ReadFileResult(Right(content)) =>
|
||||
handleFileContent(client, replyTo, content)
|
||||
unstashAll()
|
||||
timeoutCancellable.cancel()
|
||||
|
||||
case ReadFileResult(Left(failure)) =>
|
||||
replyTo ! OpenFileResponse(Left(failure))
|
||||
timeoutCancellable.cancel()
|
||||
stop()
|
||||
|
||||
case FileReadingTimeout =>
|
||||
case IOTimeout =>
|
||||
replyTo ! OpenFileResponse(Left(OperationTimeout))
|
||||
stop()
|
||||
|
||||
@ -116,6 +119,74 @@ class CollaborativeBuffer(
|
||||
}
|
||||
|
||||
case ApplyEdit(clientId, change) =>
|
||||
edit(buffer, clients, lockHolder, clientId, change)
|
||||
|
||||
case SaveFile(clientId, _, clientVersion) =>
|
||||
saveFile(buffer, clients, lockHolder, clientId, clientVersion)
|
||||
}
|
||||
|
||||
private def saving(
|
||||
buffer: Buffer,
|
||||
clients: Map[Client.Id, Client],
|
||||
lockHolder: Option[Client],
|
||||
replyTo: ActorRef,
|
||||
timeoutCancellable: Cancellable
|
||||
): Receive = {
|
||||
case IOTimeout =>
|
||||
replyTo ! SaveFailed(OperationTimeout)
|
||||
unstashAll()
|
||||
context.become(collaborativeEditing(buffer, clients, lockHolder))
|
||||
|
||||
case WriteFileResult(Left(failure)) =>
|
||||
replyTo ! SaveFailed(failure)
|
||||
unstashAll()
|
||||
timeoutCancellable.cancel()
|
||||
context.become(collaborativeEditing(buffer, clients, lockHolder))
|
||||
|
||||
case WriteFileResult(Right(())) =>
|
||||
replyTo ! FileSaved
|
||||
unstashAll()
|
||||
timeoutCancellable.cancel()
|
||||
context.become(collaborativeEditing(buffer, clients, lockHolder))
|
||||
|
||||
case _ => stash()
|
||||
}
|
||||
|
||||
private def saveFile(
|
||||
buffer: Buffer,
|
||||
clients: Map[Id, Client],
|
||||
lockHolder: Option[Client],
|
||||
clientId: Id,
|
||||
clientVersion: Version
|
||||
): Unit = {
|
||||
val hasLock = lockHolder.exists(_.id == clientId)
|
||||
if (hasLock) {
|
||||
if (clientVersion == buffer.version) {
|
||||
fileManager ! FileManagerProtocol.WriteFile(
|
||||
bufferPath,
|
||||
buffer.contents.toString
|
||||
)
|
||||
|
||||
val timeoutCancellable = context.system.scheduler
|
||||
.scheduleOnce(timeout, self, IOTimeout)
|
||||
context.become(
|
||||
saving(buffer, clients, lockHolder, sender(), timeoutCancellable)
|
||||
)
|
||||
} else {
|
||||
sender() ! SaveFileInvalidVersion(clientVersion, buffer.version)
|
||||
}
|
||||
} else {
|
||||
sender() ! SaveDenied
|
||||
}
|
||||
}
|
||||
|
||||
private def edit(
|
||||
buffer: Buffer,
|
||||
clients: Map[Id, Client],
|
||||
lockHolder: Option[Client],
|
||||
clientId: Id,
|
||||
change: FileEdit
|
||||
): Unit = {
|
||||
applyEdits(buffer, lockHolder, clientId, change) match {
|
||||
case Left(failure) =>
|
||||
sender() ! failure
|
||||
@ -128,7 +199,6 @@ class CollaborativeBuffer(
|
||||
collaborativeEditing(modifiedBuffer, clients, lockHolder)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private def applyEdits(
|
||||
@ -151,7 +221,7 @@ class CollaborativeBuffer(
|
||||
if (clientVersion == serverVersion) {
|
||||
Right(())
|
||||
} else {
|
||||
Left(InvalidVersion(clientVersion, serverVersion))
|
||||
Left(TextEditInvalidVersion(clientVersion, serverVersion))
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,9 +258,9 @@ class CollaborativeBuffer(
|
||||
|
||||
private def readFile(client: Client, path: Path): Unit = {
|
||||
fileManager ! FileManagerProtocol.ReadFile(path)
|
||||
context.system.scheduler
|
||||
.scheduleOnce(timeout, self, FileReadingTimeout)
|
||||
context.become(waitingForFileContent(client, sender()))
|
||||
val timeoutCancellable = context.system.scheduler
|
||||
.scheduleOnce(timeout, self, IOTimeout)
|
||||
context.become(waitingForFileContent(client, sender(), timeoutCancellable))
|
||||
}
|
||||
|
||||
private def handleFileContent(
|
||||
@ -299,7 +369,7 @@ class CollaborativeBuffer(
|
||||
|
||||
object CollaborativeBuffer {
|
||||
|
||||
case object FileReadingTimeout
|
||||
case object IOTimeout
|
||||
|
||||
/**
|
||||
* Creates a configuration object used to create a [[CollaborativeBuffer]]
|
||||
|
@ -72,4 +72,14 @@ object TextApi {
|
||||
)
|
||||
case object WriteDeniedError extends Error(3004, "Write denied")
|
||||
|
||||
case object SaveFile extends Method("text/save") {
|
||||
case class Params(path: Path, currentVersion: Buffer.Version)
|
||||
implicit val hasParams = new HasParams[this.type] {
|
||||
type Params = SaveFile.Params
|
||||
}
|
||||
implicit val hasResult = new HasResult[this.type] {
|
||||
type Result = Unused.type
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ object TextProtocol {
|
||||
* @param clientVersion a version send by the client
|
||||
* @param serverVersion a version computed by the server
|
||||
*/
|
||||
case class InvalidVersion(
|
||||
case class TextEditInvalidVersion(
|
||||
clientVersion: Buffer.Version,
|
||||
serverVersion: Buffer.Version
|
||||
) extends ApplyEditFailure
|
||||
@ -107,4 +107,50 @@ object TextProtocol {
|
||||
*/
|
||||
case class TextDidChange(changes: List[FileEdit])
|
||||
|
||||
/** Requests the language server to save a file on behalf of a given user.
|
||||
*
|
||||
* @param clientId the client closing the file.
|
||||
* @param path the file path.
|
||||
* @param currentVersion the current version evaluated on the client side.
|
||||
*/
|
||||
case class SaveFile(
|
||||
clientId: Client.Id,
|
||||
path: Path,
|
||||
currentVersion: Buffer.Version
|
||||
)
|
||||
|
||||
/**
|
||||
* Signals the result of saving a file.
|
||||
*/
|
||||
sealed trait SaveFileResult
|
||||
|
||||
/**
|
||||
* Signals that saving a file was executed successfully.
|
||||
*/
|
||||
case object FileSaved extends SaveFileResult
|
||||
|
||||
/**
|
||||
* Signals that the client doesn't hold write lock to the buffer.
|
||||
*/
|
||||
case object SaveDenied extends SaveFileResult
|
||||
|
||||
/**
|
||||
* Signals that version provided by a client doesn't match to the version
|
||||
* computed by the server.
|
||||
*
|
||||
* @param clientVersion a version send by the client
|
||||
* @param serverVersion a version computed by the server
|
||||
*/
|
||||
case class SaveFileInvalidVersion(
|
||||
clientVersion: Buffer.Version,
|
||||
serverVersion: Buffer.Version
|
||||
) extends SaveFileResult
|
||||
|
||||
/**
|
||||
* Signals that saving a file failed due to IO error.
|
||||
*
|
||||
* @param fsFailure a filesystem failure
|
||||
*/
|
||||
case class SaveFailed(fsFailure: FileSystemFailure) extends SaveFileResult
|
||||
|
||||
}
|
||||
|
@ -1485,4 +1485,385 @@ class TextOperationsTest extends WebSocketServerTest {
|
||||
|
||||
}
|
||||
|
||||
"text/save" must {
|
||||
|
||||
"fail when a client didn't open it" in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/applyEdit",
|
||||
"id": 2,
|
||||
"params": {
|
||||
"edit": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"oldVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522",
|
||||
"newVersion": "ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3",
|
||||
"edits": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"text": "bar"
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 12 }
|
||||
},
|
||||
"text": "foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"error": { "code": 3001, "message": "File not opened" }
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"fail when a client's version doesn't match a server version" in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc" : "2.0",
|
||||
"id" : 1,
|
||||
"result" : {
|
||||
"writeCapability" : {
|
||||
"method" : "canEdit",
|
||||
"registerOptions" : {
|
||||
"path" : {
|
||||
"rootId" : $testContentRootId,
|
||||
"segments" : [
|
||||
"foo.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"content" : "123456789",
|
||||
"currentVersion" : "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/save",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"currentVersion": "ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"error": {
|
||||
"code": 3003,
|
||||
"message": "Invalid version [client version: ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3, server version: 5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522]"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"fail when a client doesn't hold a write lock" in {
|
||||
val client1 = new WsTestClient(address)
|
||||
val client2 = new WsTestClient(address)
|
||||
|
||||
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.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client2.expectJson(json"""
|
||||
{
|
||||
"jsonrpc" : "2.0",
|
||||
"id" : 1,
|
||||
"result" : {
|
||||
"writeCapability" : null,
|
||||
"content" : "123456789",
|
||||
"currentVersion" : "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client2.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/save",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"currentVersion": "ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client2.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"error": {
|
||||
"code": 3004,
|
||||
"message": "Write denied"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"persist changes from a buffer to durable storage" in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 0,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 0,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/openFile",
|
||||
"id": 1,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{
|
||||
"jsonrpc" : "2.0",
|
||||
"id" : 1,
|
||||
"result" : {
|
||||
"writeCapability" : {
|
||||
"method" : "canEdit",
|
||||
"registerOptions" : {
|
||||
"path" : {
|
||||
"rootId" : $testContentRootId,
|
||||
"segments" : [
|
||||
"foo.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"content" : "123456789",
|
||||
"currentVersion" : "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522"
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/applyEdit",
|
||||
"id": 2,
|
||||
"params": {
|
||||
"edit": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"oldVersion": "5795c3d628fd638c9835a4c79a55809f265068c88729a1a3fcdf8522",
|
||||
"newVersion": "ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3",
|
||||
"edits": [
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 0 },
|
||||
"end": { "line": 0, "character": 0 }
|
||||
},
|
||||
"text": "bar"
|
||||
},
|
||||
{
|
||||
"range": {
|
||||
"start": { "line": 0, "character": 12 },
|
||||
"end": { "line": 0, "character": 12 }
|
||||
},
|
||||
"text": "foo"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 2,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "text/save",
|
||||
"id": 3,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"currentVersion": "ebe55342f9c8b86857402797dd723fb4a2174e0b56d6ace0a6929ec3"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 3,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/read",
|
||||
"id": 4,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"result": { "contents": "bar123456789foo" }
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user