mirror of
https://github.com/enso-org/enso.git
synced 2024-11-23 08:08:34 +03:00
File Reads for the Language Server (#559)
File Reads for the Language Server
This commit is contained in:
parent
75127d07b9
commit
fe471314ec
@ -1111,9 +1111,9 @@ null
|
||||
|
||||
##### Errors
|
||||
|
||||
- **FileSystemError(errorCode=1000)** This error signals generic file system errors.
|
||||
- **ContentRootNotFoundError(errorCode=1001)** The error informs that the requested content root cannot be found.
|
||||
- **AccessDeniedError(errorCode=1002)** It signals that a user doesn't have access to a resource.
|
||||
- [`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 a user doesn't have access to a resource.
|
||||
|
||||
#### `file/read`
|
||||
This requests that the file manager component reads the contents of a specified
|
||||
@ -1142,7 +1142,11 @@ return the contents from the in-memory buffer rather than the file on disk.
|
||||
```
|
||||
|
||||
##### Errors
|
||||
TBC
|
||||
|
||||
- [`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 a user doesn't have access to a resource.
|
||||
- [`FileNotFound`](#filenotfound) informs that file cannot be found.
|
||||
|
||||
#### `file/create`
|
||||
This request asks the file manager to create the specified file system object.
|
||||
@ -1692,3 +1696,43 @@ TBC
|
||||
### Errors - Language Server
|
||||
The language server component also has its own set of errors. This section is
|
||||
not a complete specification and will be updated as new errors are added.
|
||||
|
||||
##### `FileSystemError`
|
||||
This error signals generic file system errors.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 1000,
|
||||
"message" : "File '/foo/bar' exists but is a directory"
|
||||
}
|
||||
```
|
||||
|
||||
##### `ContentRootNotFoundError`
|
||||
The error informs that the requested content root cannot be found.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 1001,
|
||||
"message" : "Content root not found"
|
||||
}
|
||||
```
|
||||
|
||||
##### `AccessDeniedError`
|
||||
It signals that a user doesn't have access to a resource.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 1002,
|
||||
"message" : "Access denied"
|
||||
}
|
||||
```
|
||||
|
||||
##### `FileNotFound`
|
||||
It signals that requested file doesn't exist.
|
||||
|
||||
```typescript
|
||||
"error" : {
|
||||
"code" : 1003,
|
||||
"message" : "File not found"
|
||||
}
|
||||
```
|
||||
|
@ -7,15 +7,10 @@ import akka.pattern.ask
|
||||
import akka.util.Timeout
|
||||
import org.enso.languageserver.ClientApi._
|
||||
import org.enso.languageserver.data.{CapabilityRegistration, Client}
|
||||
import org.enso.languageserver.filemanager.FileManagerApi.{
|
||||
FileSystemError,
|
||||
FileWrite,
|
||||
FileWriteParams
|
||||
}
|
||||
import org.enso.languageserver.filemanager.FileManagerApi.{FileRead, _}
|
||||
import org.enso.languageserver.filemanager.FileManagerProtocol.FileWriteResult
|
||||
import org.enso.languageserver.filemanager.{
|
||||
FileManagerProtocol,
|
||||
FileSystemFailure,
|
||||
FileSystemFailureMapper
|
||||
}
|
||||
import org.enso.languageserver.jsonrpc.Errors.ServiceError
|
||||
@ -69,6 +64,7 @@ object ClientApi {
|
||||
.registerRequest(AcquireCapability)
|
||||
.registerRequest(ReleaseCapability)
|
||||
.registerRequest(FileWrite)
|
||||
.registerRequest(FileRead)
|
||||
.registerNotification(ForceReleaseCapability)
|
||||
.registerNotification(GrantCapability)
|
||||
|
||||
@ -123,22 +119,57 @@ class ClientController(
|
||||
server ! LanguageProtocol.ReleaseCapability(clientId, params.id)
|
||||
sender ! ResponseResult(ReleaseCapability, id, Unused)
|
||||
|
||||
case Request(FileWrite, id, params: FileWriteParams) =>
|
||||
(server ? FileManagerProtocol.FileWrite(params.path, params.content))
|
||||
.onComplete {
|
||||
case Success(FileWriteResult(Right(()))) =>
|
||||
webActor ! ResponseResult(FileWrite, id, Unused)
|
||||
case Request(FileWrite, id, params: FileWrite.Params) =>
|
||||
writeFile(webActor, id, params)
|
||||
|
||||
case Success(FileWriteResult(Left(failure))) =>
|
||||
webActor ! ResponseError(
|
||||
Some(id),
|
||||
FileSystemFailureMapper.mapFailure(failure)
|
||||
)
|
||||
case Request(FileRead, id, params: FileRead.Params) =>
|
||||
readFile(webActor, id, params)
|
||||
|
||||
case Failure(th) =>
|
||||
log.error("An exception occurred during writing to a file", th)
|
||||
webActor ! ResponseError(Some(id), ServiceError)
|
||||
}
|
||||
}
|
||||
|
||||
private def readFile(
|
||||
webActor: ActorRef,
|
||||
id: Id,
|
||||
params: FileRead.Params
|
||||
): Unit = {
|
||||
(server ? FileManagerProtocol.FileRead(params.path)).onComplete {
|
||||
case Success(
|
||||
FileManagerProtocol.FileReadResult(Right(content: String))
|
||||
) =>
|
||||
webActor ! ResponseResult(FileRead, id, FileRead.Result(content))
|
||||
|
||||
case Success(FileManagerProtocol.FileReadResult(Left(failure))) =>
|
||||
webActor ! ResponseError(
|
||||
Some(id),
|
||||
FileSystemFailureMapper.mapFailure(failure)
|
||||
)
|
||||
|
||||
case Failure(th) =>
|
||||
log.error("An exception occurred during reading a file", th)
|
||||
webActor ! ResponseError(Some(id), ServiceError)
|
||||
}
|
||||
}
|
||||
|
||||
private def writeFile(
|
||||
webActor: ActorRef,
|
||||
id: Id,
|
||||
params: FileWrite.Params
|
||||
): Unit = {
|
||||
(server ? FileManagerProtocol.FileWrite(params.path, params.contents))
|
||||
.onComplete {
|
||||
case Success(FileWriteResult(Right(()))) =>
|
||||
webActor ! ResponseResult(FileWrite, id, Unused)
|
||||
|
||||
case Success(FileWriteResult(Left(failure))) =>
|
||||
webActor ! ResponseError(
|
||||
Some(id),
|
||||
FileSystemFailureMapper.mapFailure(failure)
|
||||
)
|
||||
|
||||
case Failure(th) =>
|
||||
log.error("An exception occurred during writing to a file", th)
|
||||
webActor ! ResponseError(Some(id), ServiceError)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import akka.actor.{Actor, ActorLogging, ActorRef, Stash}
|
||||
import cats.effect.IO
|
||||
import org.enso.languageserver.data._
|
||||
import org.enso.languageserver.filemanager.FileManagerProtocol.{
|
||||
FileRead,
|
||||
FileReadResult,
|
||||
FileWrite,
|
||||
FileWriteResult
|
||||
}
|
||||
@ -134,5 +136,19 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO])
|
||||
} yield ()
|
||||
|
||||
sender ! FileWriteResult(result)
|
||||
|
||||
case FileRead(path) =>
|
||||
val result =
|
||||
for {
|
||||
rootPath <- config.findContentRoot(path.rootId)
|
||||
content <- fs.read(path.toFile(rootPath)).unsafeRunSync()
|
||||
} yield content
|
||||
|
||||
sender ! FileReadResult(result)
|
||||
}
|
||||
/* Note [Usage of unsafe methods]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
It invokes side-effecting function, all exceptions are caught and
|
||||
explicitly returned as left side of disjunction.
|
||||
*/
|
||||
}
|
||||
|
@ -16,15 +16,30 @@ import org.enso.languageserver.jsonrpc.{
|
||||
object FileManagerApi {
|
||||
|
||||
case object FileWrite extends Method("file/write") {
|
||||
|
||||
case class Params(path: Path, contents: String)
|
||||
|
||||
implicit val hasParams = new HasParams[this.type] {
|
||||
type Params = FileWriteParams
|
||||
type Params = FileWrite.Params
|
||||
}
|
||||
implicit val hasResult = new HasResult[this.type] {
|
||||
type Result = Unused.type
|
||||
}
|
||||
}
|
||||
|
||||
case class FileWriteParams(path: Path, content: String)
|
||||
case object FileRead extends Method("file/read") {
|
||||
|
||||
case class Params(path: Path)
|
||||
|
||||
case class Result(contents: String)
|
||||
|
||||
implicit val hasParams = new HasParams[this.type] {
|
||||
type Params = FileRead.Params
|
||||
}
|
||||
implicit val hasResult = new HasResult[this.type] {
|
||||
type Result = FileRead.Result
|
||||
}
|
||||
}
|
||||
|
||||
case class FileSystemError(override val message: String)
|
||||
extends Error(1000, message)
|
||||
@ -34,4 +49,6 @@ object FileManagerApi {
|
||||
|
||||
case object AccessDeniedError extends Error(1002, "Access denied")
|
||||
|
||||
case object FileNotFoundError extends Error(1003, "File not found")
|
||||
|
||||
}
|
||||
|
@ -17,4 +17,18 @@ object FileManagerProtocol {
|
||||
*/
|
||||
case class FileWriteResult(result: Either[FileSystemFailure, Unit])
|
||||
|
||||
/**
|
||||
* Requests the Language Server read a file.
|
||||
*
|
||||
* @param path a path to a file
|
||||
*/
|
||||
case class FileRead(path: Path)
|
||||
|
||||
/**
|
||||
* Returns a result of reading a file.
|
||||
*
|
||||
* @param result either file system failure or content of a file
|
||||
*/
|
||||
case class FileReadResult(result: Either[FileSystemFailure, String])
|
||||
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.languageserver.filemanager
|
||||
|
||||
import java.io.{File, IOException}
|
||||
import java.io.{File, FileNotFoundException, IOException}
|
||||
import java.nio.file._
|
||||
|
||||
import cats.effect.Sync
|
||||
@ -25,20 +25,33 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] {
|
||||
file: File,
|
||||
content: String
|
||||
): F[Either[FileSystemFailure, Unit]] =
|
||||
Sync[F].delay { writeStringToFile(file, content) }
|
||||
Sync[F].delay {
|
||||
Either
|
||||
.catchOnly[IOException] {
|
||||
FileUtils.write(file, content, "UTF-8")
|
||||
}
|
||||
.leftMap(errorHandling)
|
||||
}
|
||||
|
||||
private def writeStringToFile(
|
||||
file: File,
|
||||
content: String
|
||||
): Either[FileSystemFailure, Unit] =
|
||||
Either
|
||||
.catchOnly[IOException](
|
||||
FileUtils.write(file, content, "UTF-8")
|
||||
)
|
||||
.leftMap {
|
||||
case _: AccessDeniedException => AccessDenied
|
||||
case ex => GenericFileSystemFailure(ex.getMessage)
|
||||
}
|
||||
.map(_ => ())
|
||||
/**
|
||||
* Reads the contents of a textual file.
|
||||
*
|
||||
* @param file path to the file
|
||||
* @return either [[FileSystemFailure]] or the content of a file as a String
|
||||
*/
|
||||
override def read(file: File): F[Either[FileSystemFailure, String]] =
|
||||
Sync[F].delay {
|
||||
Either
|
||||
.catchOnly[IOException] {
|
||||
FileUtils.readFileToString(file, "UTF-8")
|
||||
}
|
||||
.leftMap(errorHandling)
|
||||
}
|
||||
|
||||
private val errorHandling: IOException => FileSystemFailure = {
|
||||
case _: FileNotFoundException => FileNotFound
|
||||
case _: AccessDeniedException => AccessDenied
|
||||
case ex => GenericFileSystemFailure(ex.getMessage)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -14,11 +14,19 @@ trait FileSystemApi[F[_]] {
|
||||
*
|
||||
* @param file path to the file
|
||||
* @param content a textual content of the file
|
||||
* @return either FileSystemFailure or Unit
|
||||
* @return either [[FileSystemFailure]] or Unit
|
||||
*/
|
||||
def write(
|
||||
file: File,
|
||||
content: String
|
||||
): F[Either[FileSystemFailure, Unit]]
|
||||
|
||||
/**
|
||||
* Reads the contents of a textual file.
|
||||
*
|
||||
* @param file path to the file
|
||||
* @return either [[FileSystemFailure]] or the content of a file as a String
|
||||
*/
|
||||
def read(file: File): F[Either[FileSystemFailure, String]]
|
||||
|
||||
}
|
||||
|
@ -15,6 +15,11 @@ case object ContentRootNotFound extends FileSystemFailure
|
||||
*/
|
||||
case object AccessDenied extends FileSystemFailure
|
||||
|
||||
/**
|
||||
* Signals that the file cannot be found.
|
||||
*/
|
||||
case object FileNotFound extends FileSystemFailure
|
||||
|
||||
/**
|
||||
* Signals file system specific errors.
|
||||
*
|
||||
|
@ -3,16 +3,24 @@ package org.enso.languageserver.filemanager
|
||||
import org.enso.languageserver.filemanager.FileManagerApi.{
|
||||
AccessDeniedError,
|
||||
ContentRootNotFoundError,
|
||||
FileNotFoundError,
|
||||
FileSystemError
|
||||
}
|
||||
import org.enso.languageserver.jsonrpc.Error
|
||||
|
||||
object FileSystemFailureMapper {
|
||||
|
||||
/**
|
||||
* Maps [[FileSystemFailure]] into JSON RPC error.
|
||||
*
|
||||
* @param fileSystemFailure file system specific failure
|
||||
* @return JSON RPC error
|
||||
*/
|
||||
def mapFailure(fileSystemFailure: FileSystemFailure): Error =
|
||||
fileSystemFailure match {
|
||||
case ContentRootNotFound => ContentRootNotFoundError
|
||||
case AccessDenied => AccessDeniedError
|
||||
case FileNotFound => FileNotFoundError
|
||||
case GenericFileSystemFailure(reason) => FileSystemError(reason)
|
||||
}
|
||||
|
||||
|
@ -240,7 +240,7 @@ class WebSocketServerTest
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo", "bar", "baz.txt" ]
|
||||
},
|
||||
"content": "123456789"
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
@ -267,7 +267,7 @@ class WebSocketServerTest
|
||||
"rootId": ${UUID.randomUUID()},
|
||||
"segments": [ "foo", "bar", "baz.txt" ]
|
||||
},
|
||||
"content": "123456789"
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
@ -283,6 +283,74 @@ class WebSocketServerTest
|
||||
client.expectNoMessage()
|
||||
}
|
||||
|
||||
"read a file content" in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/write",
|
||||
"id": 4,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
},
|
||||
"contents": "123456789"
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 4,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/read",
|
||||
"id": 5,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 5,
|
||||
"result": { "contents": "123456789" }
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
"return FileNotFoundError if a file doesn't exist" in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/read",
|
||||
"id": 6,
|
||||
"params": {
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "bar.txt" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 6,
|
||||
"error" : {
|
||||
"code" : 1003,
|
||||
"message" : "File not found"
|
||||
}
|
||||
}
|
||||
""")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WsTestClient(address: String) {
|
||||
|
@ -48,6 +48,26 @@ class FileSystemSpec extends AnyFlatSpec with Matchers {
|
||||
readTxtFile(path) shouldBe content
|
||||
}
|
||||
|
||||
it should "return FileNotFound failure if the file doesn't exist" in new TestCtx {
|
||||
//given
|
||||
val path = Paths.get(testDirPath.toString, "foo.txt")
|
||||
//when
|
||||
val result = objectUnderTest.read(path.toFile).unsafeRunSync()
|
||||
//then
|
||||
result shouldBe Left(FileNotFound)
|
||||
}
|
||||
|
||||
it should "read a file content" in new TestCtx {
|
||||
//given
|
||||
val path = Paths.get(testDirPath.toString, "foo.txt")
|
||||
val content = "123456789"
|
||||
objectUnderTest.write(path.toFile, content).unsafeRunSync()
|
||||
//when
|
||||
val result = objectUnderTest.read(path.toFile).unsafeRunSync()
|
||||
//then
|
||||
result shouldBe Right(content)
|
||||
}
|
||||
|
||||
def readTxtFile(path: Path): String = {
|
||||
val buffer = Source.fromFile(path.toFile)
|
||||
val content = buffer.getLines().mkString
|
||||
|
Loading…
Reference in New Issue
Block a user