mirror of
https://github.com/enso-org/enso.git
synced 2024-11-27 06:03:23 +03:00
File and directory creation for LS (#560)
This commit is contained in:
parent
fe471314ec
commit
0b22606fa1
@ -1171,7 +1171,10 @@ null
|
||||
```
|
||||
|
||||
##### 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.
|
||||
|
||||
#### `file/delete`
|
||||
This request asks the file manager to delete the specified file system object.
|
||||
|
@ -7,8 +7,11 @@ 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.{FileRead, _}
|
||||
import org.enso.languageserver.filemanager.FileManagerProtocol.FileWriteResult
|
||||
import org.enso.languageserver.filemanager.FileManagerApi._
|
||||
import org.enso.languageserver.filemanager.FileManagerProtocol.{
|
||||
CreateFileResult,
|
||||
WriteFileResult
|
||||
}
|
||||
import org.enso.languageserver.filemanager.{
|
||||
FileManagerProtocol,
|
||||
FileSystemFailureMapper
|
||||
@ -63,8 +66,9 @@ object ClientApi {
|
||||
val protocol: Protocol = Protocol.empty
|
||||
.registerRequest(AcquireCapability)
|
||||
.registerRequest(ReleaseCapability)
|
||||
.registerRequest(FileWrite)
|
||||
.registerRequest(FileRead)
|
||||
.registerRequest(WriteFile)
|
||||
.registerRequest(ReadFile)
|
||||
.registerRequest(CreateFile)
|
||||
.registerNotification(ForceReleaseCapability)
|
||||
.registerNotification(GrantCapability)
|
||||
|
||||
@ -119,26 +123,28 @@ class ClientController(
|
||||
server ! LanguageProtocol.ReleaseCapability(clientId, params.id)
|
||||
sender ! ResponseResult(ReleaseCapability, id, Unused)
|
||||
|
||||
case Request(FileWrite, id, params: FileWrite.Params) =>
|
||||
case Request(WriteFile, id, params: WriteFile.Params) =>
|
||||
writeFile(webActor, id, params)
|
||||
|
||||
case Request(FileRead, id, params: FileRead.Params) =>
|
||||
case Request(ReadFile, id, params: ReadFile.Params) =>
|
||||
readFile(webActor, id, params)
|
||||
|
||||
case Request(CreateFile, id, params: CreateFile.Params) =>
|
||||
createFile(webActor, id, params)
|
||||
}
|
||||
|
||||
private def readFile(
|
||||
webActor: ActorRef,
|
||||
id: Id,
|
||||
params: FileRead.Params
|
||||
params: ReadFile.Params
|
||||
): Unit = {
|
||||
(server ? FileManagerProtocol.FileRead(params.path)).onComplete {
|
||||
(server ? FileManagerProtocol.ReadFile(params.path)).onComplete {
|
||||
case Success(
|
||||
FileManagerProtocol.FileReadResult(Right(content: String))
|
||||
FileManagerProtocol.ReadFileResult(Right(content: String))
|
||||
) =>
|
||||
webActor ! ResponseResult(FileRead, id, FileRead.Result(content))
|
||||
webActor ! ResponseResult(ReadFile, id, ReadFile.Result(content))
|
||||
|
||||
case Success(FileManagerProtocol.FileReadResult(Left(failure))) =>
|
||||
case Success(FileManagerProtocol.ReadFileResult(Left(failure))) =>
|
||||
webActor ! ResponseError(
|
||||
Some(id),
|
||||
FileSystemFailureMapper.mapFailure(failure)
|
||||
@ -153,14 +159,14 @@ class ClientController(
|
||||
private def writeFile(
|
||||
webActor: ActorRef,
|
||||
id: Id,
|
||||
params: FileWrite.Params
|
||||
params: WriteFile.Params
|
||||
): Unit = {
|
||||
(server ? FileManagerProtocol.FileWrite(params.path, params.contents))
|
||||
(server ? FileManagerProtocol.WriteFile(params.path, params.contents))
|
||||
.onComplete {
|
||||
case Success(FileWriteResult(Right(()))) =>
|
||||
webActor ! ResponseResult(FileWrite, id, Unused)
|
||||
case Success(WriteFileResult(Right(()))) =>
|
||||
webActor ! ResponseResult(WriteFile, id, Unused)
|
||||
|
||||
case Success(FileWriteResult(Left(failure))) =>
|
||||
case Success(WriteFileResult(Left(failure))) =>
|
||||
webActor ! ResponseError(
|
||||
Some(id),
|
||||
FileSystemFailureMapper.mapFailure(failure)
|
||||
@ -172,4 +178,26 @@ class ClientController(
|
||||
}
|
||||
}
|
||||
|
||||
private def createFile(
|
||||
webActor: ActorRef,
|
||||
id: Id,
|
||||
params: CreateFile.Params
|
||||
): Unit = {
|
||||
(server ? FileManagerProtocol.CreateFile(params.`object`))
|
||||
.onComplete {
|
||||
case Success(CreateFileResult(Right(()))) =>
|
||||
webActor ! ResponseResult(CreateFile, id, Unused)
|
||||
|
||||
case Success(CreateFileResult(Left(failure))) =>
|
||||
webActor ! ResponseError(
|
||||
Some(id),
|
||||
FileSystemFailureMapper.mapFailure(failure)
|
||||
)
|
||||
|
||||
case Failure(th) =>
|
||||
log.error("An exception occurred during creating a file", th)
|
||||
webActor ! ResponseError(Some(id), ServiceError)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,17 +1,10 @@
|
||||
package org.enso.languageserver
|
||||
|
||||
import java.io.File
|
||||
|
||||
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
|
||||
}
|
||||
import org.enso.languageserver.filemanager.{FileSystemApi, FileSystemFailure}
|
||||
import org.enso.languageserver.filemanager.FileManagerProtocol._
|
||||
import org.enso.languageserver.filemanager.{FileSystemApi, FileSystemObject}
|
||||
|
||||
object LanguageProtocol {
|
||||
|
||||
@ -128,23 +121,42 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO])
|
||||
initialized(config, env.releaseCapability(clientId, capabilityId))
|
||||
)
|
||||
|
||||
case FileWrite(path, content) =>
|
||||
case WriteFile(path, content) =>
|
||||
val result =
|
||||
for {
|
||||
rootPath <- config.findContentRoot(path.rootId)
|
||||
_ <- fs.write(path.toFile(rootPath), content).unsafeRunSync()
|
||||
} yield ()
|
||||
|
||||
sender ! FileWriteResult(result)
|
||||
sender ! WriteFileResult(result)
|
||||
|
||||
case FileRead(path) =>
|
||||
case ReadFile(path) =>
|
||||
val result =
|
||||
for {
|
||||
rootPath <- config.findContentRoot(path.rootId)
|
||||
content <- fs.read(path.toFile(rootPath)).unsafeRunSync()
|
||||
} yield content
|
||||
|
||||
sender ! FileReadResult(result)
|
||||
sender ! ReadFileResult(result)
|
||||
|
||||
case CreateFile(FileSystemObject.File(name, path)) =>
|
||||
val result =
|
||||
for {
|
||||
rootPath <- config.findContentRoot(path.rootId)
|
||||
_ <- fs.createFile(path.toFile(rootPath, name)).unsafeRunSync()
|
||||
} yield ()
|
||||
|
||||
sender ! CreateFileResult(result)
|
||||
|
||||
case CreateFile(FileSystemObject.Directory(name, path)) =>
|
||||
val result =
|
||||
for {
|
||||
rootPath <- config.findContentRoot(path.rootId)
|
||||
_ <- fs.createDirectory(path.toFile(rootPath, name)).unsafeRunSync()
|
||||
} yield ()
|
||||
|
||||
sender ! CreateFileResult(result)
|
||||
|
||||
}
|
||||
/* Note [Usage of unsafe methods]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
@ -15,29 +15,41 @@ import org.enso.languageserver.jsonrpc.{
|
||||
*/
|
||||
object FileManagerApi {
|
||||
|
||||
case object FileWrite extends Method("file/write") {
|
||||
case object WriteFile extends Method("file/write") {
|
||||
|
||||
case class Params(path: Path, contents: String)
|
||||
|
||||
implicit val hasParams = new HasParams[this.type] {
|
||||
type Params = FileWrite.Params
|
||||
type Params = WriteFile.Params
|
||||
}
|
||||
implicit val hasResult = new HasResult[this.type] {
|
||||
type Result = Unused.type
|
||||
}
|
||||
}
|
||||
|
||||
case object FileRead extends Method("file/read") {
|
||||
case object ReadFile 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
|
||||
type Params = ReadFile.Params
|
||||
}
|
||||
implicit val hasResult = new HasResult[this.type] {
|
||||
type Result = FileRead.Result
|
||||
type Result = ReadFile.Result
|
||||
}
|
||||
}
|
||||
|
||||
case object CreateFile extends Method("file/create") {
|
||||
|
||||
case class Params(`object`: FileSystemObject)
|
||||
|
||||
implicit val hasParams = new HasParams[this.type] {
|
||||
type Params = CreateFile.Params
|
||||
}
|
||||
implicit val hasResult = new HasResult[this.type] {
|
||||
type Result = Unused.type
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,27 +8,41 @@ object FileManagerProtocol {
|
||||
* @param path a path to a file
|
||||
* @param content a textual content
|
||||
*/
|
||||
case class FileWrite(path: Path, content: String)
|
||||
case class WriteFile(path: Path, content: String)
|
||||
|
||||
/**
|
||||
* Signals file manipulation status.
|
||||
*
|
||||
* @param result either file system failure or unit representing success
|
||||
*/
|
||||
case class FileWriteResult(result: Either[FileSystemFailure, Unit])
|
||||
case class WriteFileResult(result: Either[FileSystemFailure, Unit])
|
||||
|
||||
/**
|
||||
* Requests the Language Server read a file.
|
||||
*
|
||||
* @param path a path to a file
|
||||
*/
|
||||
case class FileRead(path: Path)
|
||||
case class ReadFile(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])
|
||||
case class ReadFileResult(result: Either[FileSystemFailure, String])
|
||||
|
||||
/**
|
||||
* Requests the Language Server create a file system object.
|
||||
*
|
||||
* @param `object` a file system object
|
||||
*/
|
||||
case class CreateFile(`object`: FileSystemObject)
|
||||
|
||||
/**
|
||||
* Returns a result of creating a file system object.
|
||||
*
|
||||
* @param result either file system failure or unit representing success
|
||||
*/
|
||||
case class CreateFileResult(result: Either[FileSystemFailure, Unit])
|
||||
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package org.enso.languageserver.filemanager
|
||||
import java.io.{File, FileNotFoundException, IOException}
|
||||
import java.nio.file._
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.effect.Sync
|
||||
import cats.implicits._
|
||||
import org.apache.commons.io.FileUtils
|
||||
@ -48,6 +49,50 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] {
|
||||
.leftMap(errorHandling)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an empty file with parent directory.
|
||||
*
|
||||
* @param file path to the file
|
||||
* @return
|
||||
*/
|
||||
override def createFile(file: File): F[Either[FileSystemFailure, Unit]] = {
|
||||
val op =
|
||||
for {
|
||||
_ <- EitherT { createDirectory(file.getParentFile) }
|
||||
_ <- EitherT { createEmptyFile(file) }
|
||||
} yield ()
|
||||
|
||||
op.value
|
||||
}
|
||||
|
||||
private def createEmptyFile(file: File): F[Either[FileSystemFailure, Unit]] =
|
||||
Sync[F].delay {
|
||||
Either
|
||||
.catchOnly[IOException] {
|
||||
file.createNewFile()
|
||||
}
|
||||
.leftMap(errorHandling)
|
||||
.map(_ => ())
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a directory, including any necessary but nonexistent parent
|
||||
* directories.
|
||||
*
|
||||
* @param file path to the file
|
||||
* @return
|
||||
*/
|
||||
override def createDirectory(
|
||||
file: File
|
||||
): F[Either[FileSystemFailure, Unit]] =
|
||||
Sync[F].delay {
|
||||
Either
|
||||
.catchOnly[IOException] {
|
||||
FileUtils.forceMkdir(file)
|
||||
}
|
||||
.leftMap(errorHandling)
|
||||
}
|
||||
|
||||
private val errorHandling: IOException => FileSystemFailure = {
|
||||
case _: FileNotFoundException => FileNotFound
|
||||
case _: AccessDeniedException => AccessDenied
|
||||
|
@ -29,4 +29,21 @@ trait FileSystemApi[F[_]] {
|
||||
*/
|
||||
def read(file: File): F[Either[FileSystemFailure, String]]
|
||||
|
||||
/**
|
||||
* Creates an empty file with parent directory.
|
||||
*
|
||||
* @param file path to the file
|
||||
* @return
|
||||
*/
|
||||
def createFile(file: File): F[Either[FileSystemFailure, Unit]]
|
||||
|
||||
/**
|
||||
* Creates a directory, including any necessary but nonexistent parent
|
||||
* directories.
|
||||
*
|
||||
* @param file path to the file
|
||||
* @return
|
||||
*/
|
||||
def createDirectory(file: File): F[Either[FileSystemFailure, Unit]]
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,74 @@
|
||||
package org.enso.languageserver.filemanager
|
||||
|
||||
import io.circe.generic.auto._
|
||||
import io.circe.syntax._
|
||||
import io.circe.{Decoder, Encoder, Json}
|
||||
|
||||
/**
|
||||
* A representation of filesystem object.
|
||||
*/
|
||||
sealed trait FileSystemObject
|
||||
|
||||
object FileSystemObject {
|
||||
|
||||
/**
|
||||
* Represents a directory.
|
||||
*
|
||||
* @param name a name of the directory
|
||||
* @param path a path to the directory
|
||||
*/
|
||||
case class Directory(name: String, path: Path) extends FileSystemObject
|
||||
|
||||
/**
|
||||
* Represents a file.
|
||||
*
|
||||
* @param name a name of the file
|
||||
* @param path a path to the file
|
||||
*/
|
||||
case class File(name: String, path: Path) extends FileSystemObject
|
||||
|
||||
private val TypeField = "type"
|
||||
|
||||
private val NameField = "name"
|
||||
|
||||
private val PathField = "path"
|
||||
|
||||
private val FileType = "File"
|
||||
|
||||
private val DirectoryType = "Directory"
|
||||
|
||||
implicit val fsoDecoder: Decoder[FileSystemObject] =
|
||||
Decoder.instance { cursor =>
|
||||
cursor.downField(TypeField).as[String].flatMap {
|
||||
case FileType =>
|
||||
for {
|
||||
name <- cursor.downField(NameField).as[String]
|
||||
path <- cursor.downField(PathField).as[Path]
|
||||
} yield File(name, path)
|
||||
|
||||
case DirectoryType =>
|
||||
for {
|
||||
name <- cursor.downField(NameField).as[String]
|
||||
path <- cursor.downField(PathField).as[Path]
|
||||
} yield Directory(name, path)
|
||||
}
|
||||
}
|
||||
|
||||
implicit val fsoEncoder: Encoder[FileSystemObject] =
|
||||
Encoder.instance[FileSystemObject] {
|
||||
case Directory(name, path) =>
|
||||
Json.obj(
|
||||
TypeField -> DirectoryType.asJson,
|
||||
NameField -> name.asJson,
|
||||
PathField -> path.asJson
|
||||
)
|
||||
|
||||
case File(name, path) =>
|
||||
Json.obj(
|
||||
TypeField -> FileType.asJson,
|
||||
NameField -> name.asJson,
|
||||
PathField -> path.asJson
|
||||
)
|
||||
}
|
||||
|
||||
}
|
@ -16,4 +16,9 @@ case class Path(rootId: UUID, segments: List[String]) {
|
||||
case (parent, child) => new File(parent, child)
|
||||
}
|
||||
|
||||
def toFile(rootPath: File, fileName: String): File = {
|
||||
val parentDir = toFile(rootPath)
|
||||
new File(parentDir, fileName)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -351,6 +351,66 @@ class WebSocketServerTest
|
||||
""")
|
||||
}
|
||||
|
||||
"create a file" in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/create",
|
||||
"id": 7,
|
||||
"params": {
|
||||
"object": {
|
||||
"type": "File",
|
||||
"name": "bar.txt",
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo1" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
val file = Paths.get(testContentRoot.toString, "foo1", "bar.txt").toFile
|
||||
file.isFile shouldBe true
|
||||
}
|
||||
|
||||
"create a directory" in {
|
||||
val client = new WsTestClient(address)
|
||||
|
||||
client.send(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"method": "file/create",
|
||||
"id": 7,
|
||||
"params": {
|
||||
"object": {
|
||||
"type": "Directory",
|
||||
"name": "baz",
|
||||
"path": {
|
||||
"rootId": $testContentRootId,
|
||||
"segments": [ "foo1" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""")
|
||||
client.expectJson(json"""
|
||||
{ "jsonrpc": "2.0",
|
||||
"id": 7,
|
||||
"result": null
|
||||
}
|
||||
""")
|
||||
|
||||
val file = Paths.get(testContentRoot.toString, "foo1", "baz").toFile
|
||||
file.isDirectory shouldBe true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WsTestClient(address: String) {
|
||||
|
@ -68,6 +68,27 @@ class FileSystemSpec extends AnyFlatSpec with Matchers {
|
||||
result shouldBe Right(content)
|
||||
}
|
||||
|
||||
it should "create a directory" in new TestCtx {
|
||||
//given
|
||||
val path = Paths.get(testDirPath.toString, "foo", "bar")
|
||||
//when
|
||||
val result = objectUnderTest.createDirectory(path.toFile).unsafeRunSync()
|
||||
//then
|
||||
result shouldBe Right(())
|
||||
path.toFile.isDirectory shouldBe true
|
||||
}
|
||||
|
||||
it should "create an empty file" in new TestCtx {
|
||||
//given
|
||||
val path = Paths.get(testDirPath.toString, "foo", "bar", "baz.txt")
|
||||
//when
|
||||
val result = objectUnderTest.createFile(path.toFile).unsafeRunSync()
|
||||
//then
|
||||
result shouldBe Right(())
|
||||
path.toFile.getParentFile.isDirectory shouldBe true
|
||||
path.toFile.isFile shouldBe true
|
||||
}
|
||||
|
||||
def readTxtFile(path: Path): String = {
|
||||
val buffer = Source.fromFile(path.toFile)
|
||||
val content = buffer.getLines().mkString
|
||||
|
Loading…
Reference in New Issue
Block a user