From 22f505b6c4efe58ee010aaa285e5c411efa0a7d4 Mon Sep 17 00:00:00 2001 From: Dmitry Bushev Date: Mon, 9 Mar 2020 13:54:44 +0300 Subject: [PATCH] Add file/exists message to language server (#579) * feat: impl file/exists * doc: update engine-services * doc: wrap 80 --- doc/design/engine/engine-services.md | 9 +++--- .../languageserver/ClientController.scala | 25 ++++++++++++++++ .../enso/languageserver/LanguageServer.scala | 9 ++++++ .../filemanager/FileManagerApi.scala | 14 +++++++++ .../filemanager/FileManagerProtocol.scala | 13 +++++++++ .../filemanager/FileSystem.scala | 15 ++++++++++ .../filemanager/FileSystemApi.scala | 8 +++++ .../filemanager/FileSystemSpec.scala | 21 ++++++++++++++ .../websocket/FileManagerTest.scala | 29 +++++++++++++++++++ 9 files changed, 139 insertions(+), 4 deletions(-) diff --git a/doc/design/engine/engine-services.md b/doc/design/engine/engine-services.md index f6326e1e4cf..89c6e5e2544 100644 --- a/doc/design/engine/engine-services.md +++ b/doc/design/engine/engine-services.md @@ -1334,7 +1334,8 @@ at the specified path. ``` ##### Errors -TBC +- [`ContentRootNotFoundError`](#contentrootnotfounderror) to signal that the + requested content root cannot be found. #### `file/tree` This request asks the file manager component to generate and provide the @@ -1599,7 +1600,7 @@ null ``` ##### Errors -- [`FileNotOpenedError`](#filenotopenederror) to signal that a file wasn't +- [`FileNotOpenedError`](#filenotopenederror) to signal that a file wasn't opened. #### `text/save` @@ -1766,7 +1767,7 @@ the language server. This is incredibly important for enabling the high levels of interactivity required by Enso Studio. #### 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. ##### `ExpressionId` @@ -2051,4 +2052,4 @@ Signals that a file wasn't opened. "code" : 3001, "message" : "File not opened" } -``` \ No newline at end of file +``` diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/ClientController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/ClientController.scala index 83c4d14f84b..48fbcbf0a5f 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/ClientController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/ClientController.scala @@ -52,6 +52,7 @@ object ClientApi { .registerRequest(CloseFile) .registerRequest(DeleteFile) .registerRequest(CopyFile) + .registerRequest(ExistsFile) .registerNotification(ForceReleaseCapability) .registerNotification(GrantCapability) @@ -130,6 +131,9 @@ class ClientController( case Request(CopyFile, id, params: CopyFile.Params) => copyFile(webActor, id, params) + + case Request(ExistsFile, id, params: ExistsFile.Params) => + existsFile(webActor, id, params) } 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) + } + } } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala b/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala index c470a9e4995..df0d8130368 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/LanguageServer.scala @@ -111,6 +111,15 @@ class LanguageServer(config: Config, fs: FileSystemApi[IO]) } yield () 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] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala index ce273eedd08..b8aca41d6e6 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerApi.scala @@ -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 case class FileSystemError(override val message: String) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerProtocol.scala index 101025576e2..f14dfe46656 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileManagerProtocol.scala @@ -74,4 +74,17 @@ object FileManagerProtocol { */ 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]) } diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala index ed3a1c0b5b5..f9cd31d489e 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystem.scala @@ -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 = { case _: FileNotFoundException => FileNotFound case _: NoSuchFileException => FileNotFound diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala index 22e433fb034..c7de128658b 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileSystemApi.scala @@ -66,4 +66,12 @@ trait FileSystemApi[F[_]] { to: File ): 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]] + } diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala b/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala index 70ed6ae3c75..d2445733348 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/filemanager/FileSystemSpec.scala @@ -233,6 +233,27 @@ class FileSystemSpec extends AnyFlatSpec with Matchers { 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 = { val buffer = Source.fromFile(path.toFile) val content = buffer.getLines().mkString diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala index 3c600c53243..c68a2e449a7 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/FileManagerTest.scala @@ -507,6 +507,35 @@ class FileManagerTest extends WebSocketServerTest { val to = Paths.get(testContentRoot.toString, "some", "test.txt") 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 + } + } + """) + } + } }