Add file/exists message to language server (#579)

* feat: impl file/exists

* doc: update engine-services

* doc: wrap 80
This commit is contained in:
Dmitry Bushev 2020-03-09 13:54:44 +03:00 committed by GitHub
parent 31d5e6eb5b
commit 22f505b6c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 4 deletions

View File

@ -1334,7 +1334,8 @@ at the specified path.
``` ```
##### Errors ##### Errors
TBC - [`ContentRootNotFoundError`](#contentrootnotfounderror) to signal that the
requested content root cannot be found.
#### `file/tree` #### `file/tree`
This request asks the file manager component to generate and provide the This request asks the file manager component to generate and provide the
@ -1599,7 +1600,7 @@ null
``` ```
##### Errors ##### Errors
- [`FileNotOpenedError`](#filenotopenederror) to signal that a file wasn't - [`FileNotOpenedError`](#filenotopenederror) to signal that a file wasn't
opened. opened.
#### `text/save` #### `text/save`
@ -1766,7 +1767,7 @@ the language server. This is incredibly important for enabling the high levels
of interactivity required by Enso Studio. of interactivity required by Enso Studio.
#### Types #### Types
The execution management API exposes a set of common types used by many of its The execution management API exposes a set of common types used by many of its
messages. messages.
##### `ExpressionId` ##### `ExpressionId`
@ -2051,4 +2052,4 @@ Signals that a file wasn't opened.
"code" : 3001, "code" : 3001,
"message" : "File not opened" "message" : "File not opened"
} }
``` ```

View File

@ -52,6 +52,7 @@ object ClientApi {
.registerRequest(CloseFile) .registerRequest(CloseFile)
.registerRequest(DeleteFile) .registerRequest(DeleteFile)
.registerRequest(CopyFile) .registerRequest(CopyFile)
.registerRequest(ExistsFile)
.registerNotification(ForceReleaseCapability) .registerNotification(ForceReleaseCapability)
.registerNotification(GrantCapability) .registerNotification(GrantCapability)
@ -130,6 +131,9 @@ class ClientController(
case Request(CopyFile, id, params: CopyFile.Params) => case Request(CopyFile, id, params: CopyFile.Params) =>
copyFile(webActor, id, params) copyFile(webActor, id, params)
case Request(ExistsFile, id, params: ExistsFile.Params) =>
existsFile(webActor, id, params)
} }
private def readFile( private def readFile(
@ -243,4 +247,25 @@ class ClientController(
} }
} }
private def existsFile(
webActor: ActorRef,
id: Id,
params: ExistsFile.Params
): Unit = {
(server ? FileManagerProtocol.ExistsFile(params.path))
.onComplete {
case Success(FileManagerProtocol.ExistsFileResult(Right(exists))) =>
webActor ! ResponseResult(ExistsFile, id, ExistsFile.Result(exists))
case Success(FileManagerProtocol.ExistsFileResult(Left(failure))) =>
webActor ! ResponseError(
Some(id),
FileSystemFailureMapper.mapFailure(failure)
)
case Failure(th) =>
log.error("An exception occurred during exists file command", th)
webActor ! ResponseError(Some(id), ServiceError)
}
}
} }

View File

@ -111,6 +111,15 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO])
} yield () } yield ()
sender ! CopyFileResult(result) sender ! CopyFileResult(result)
case ExistsFile(path) =>
val result =
for {
rootPath <- config.findContentRoot(path.rootId)
exists <- fs.exists(path.toFile(rootPath)).unsafeRunSync()
} yield exists
sender ! ExistsFileResult(result)
} }
/* Note [Usage of unsafe methods] /* Note [Usage of unsafe methods]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -77,6 +77,20 @@ object FileManagerApi {
} }
} }
case object ExistsFile extends Method("file/exists") {
case class Params(path: Path)
case class Result(exists: Boolean)
implicit val hasParams = new HasParams[this.type] {
type Params = ExistsFile.Params
}
implicit val hasResult = new HasResult[this.type] {
type Result = ExistsFile.Result
}
}
// Errors // Errors
case class FileSystemError(override val message: String) case class FileSystemError(override val message: String)

View File

@ -74,4 +74,17 @@ object FileManagerProtocol {
*/ */
case class CopyFileResult(result: Either[FileSystemFailure, Unit]) case class CopyFileResult(result: Either[FileSystemFailure, Unit])
/**
* Requests the Language Server to check the existence of file system object.
*
* @param path a path to a file
*/
case class ExistsFile(path: Path)
/**
* Returns a result of checking the existence of file system object.
*
* @param result either file system failure or file existence flag
*/
case class ExistsFileResult(result: Either[FileSystemFailure, Boolean])
} }

View File

@ -143,6 +143,21 @@ class FileSystem[F[_]: Sync] extends FileSystemApi[F] {
} }
} }
/**
* Checks if the specified file exists.
*
* @param file path to the file or directory
* @return either [[FileSystemFailure]] or file existence flag
*/
override def exists(file: File): F[Either[FileSystemFailure, Boolean]] =
Sync[F].delay {
Either
.catchOnly[IOException] {
Files.exists(file.toPath)
}
.leftMap(errorHandling)
}
private val errorHandling: IOException => FileSystemFailure = { private val errorHandling: IOException => FileSystemFailure = {
case _: FileNotFoundException => FileNotFound case _: FileNotFoundException => FileNotFound
case _: NoSuchFileException => FileNotFound case _: NoSuchFileException => FileNotFound

View File

@ -66,4 +66,12 @@ trait FileSystemApi[F[_]] {
to: File to: File
): F[Either[FileSystemFailure, Unit]] ): F[Either[FileSystemFailure, Unit]]
/**
* Checks if the specified file exists.
*
* @param file path to the file or directory
* @return either [[FileSystemFailure]] or file existence flag
*/
def exists(file: File): F[Either[FileSystemFailure, Boolean]]
} }

View File

@ -233,6 +233,27 @@ class FileSystemSpec extends AnyFlatSpec with Matchers {
to.toFile.exists shouldBe false to.toFile.exists shouldBe false
} }
it should "check file existence" in new TestCtx {
//given
val path = Paths.get(testDirPath.toString, "foo", "bar.txt")
createEmptyFile(path)
path.toFile.isFile shouldBe true
//when
val result = objectUnderTest.exists(path.toFile).unsafeRunSync()
//then
result shouldBe Right(true)
}
it should "check file non-existence" in new TestCtx {
//given
val path = Paths.get(testDirPath.toString, "nonexistent.txt")
path.toFile.exists shouldBe false
//when
val result = objectUnderTest.exists(path.toFile).unsafeRunSync()
//then
result shouldBe Right(false)
}
def readTxtFile(path: Path): String = { def readTxtFile(path: Path): String = {
val buffer = Source.fromFile(path.toFile) val buffer = Source.fromFile(path.toFile)
val content = buffer.getLines().mkString val content = buffer.getLines().mkString

View File

@ -507,6 +507,35 @@ class FileManagerTest extends WebSocketServerTest {
val to = Paths.get(testContentRoot.toString, "some", "test.txt") val to = Paths.get(testContentRoot.toString, "some", "test.txt")
to.toFile.isFile shouldBe false to.toFile.isFile shouldBe false
} }
"check file existence" in {
val client = new WsTestClient(address)
val path = Paths.get(testContentRoot.toString, "nonexistent.txt")
path.toFile.exists shouldBe false
// check file exists
client.send(json"""
{ "jsonrpc": "2.0",
"method": "file/exists",
"id": 27,
"params": {
"path": {
"rootId": $testContentRootId,
"segments": [ "nonexistent.txt" ]
}
}
}
""")
client.expectJson(json"""
{ "jsonrpc": "2.0",
"id": 27,
"result": {
"exists": false
}
}
""")
}
} }
} }