diff --git a/docs/language-server/protocol-language-server.md b/docs/language-server/protocol-language-server.md index 2bd4ee5e8e2..4f4aac89bf8 100644 --- a/docs/language-server/protocol-language-server.md +++ b/docs/language-server/protocol-language-server.md @@ -2350,6 +2350,7 @@ of the (possibly multiple) content roots. interface FileEventNotification { path: Path; kind: FileEventKind; + attributes?: FileAttributes; } ``` diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileEvent.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileEvent.scala index cb788171b1e..1adfe683ca8 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileEvent.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/FileEvent.scala @@ -8,8 +8,13 @@ import org.enso.filewatcher.Watcher * * @param path path to the file system object * @param kind type of file system event + * @param attributes the file attributes */ -case class FileEvent(path: Path, kind: FileEventKind) +case class FileEvent( + path: Path, + kind: FileEventKind, + attributes: Either[FileSystemFailure, FileAttributes] +) object FileEvent { @@ -18,21 +23,27 @@ object FileEvent { * @param root a project root * @param base a watched path * @param event a file system event + * @param attributes a file attributes * @return file event */ def fromWatcherEvent( root: File, base: Path, - event: Watcher.WatcherEvent - ): FileEvent = + event: Watcher.WatcherEvent, + attributes: Either[FileSystemFailure, FileSystemApi.Attributes] + ): FileEvent = { + val eventPath = Path.getRelativePath(root, base, event.path) FileEvent( - Path.getRelativePath(root, base, event.path), - FileEventKind(event.eventType) + eventPath, + FileEventKind(event.eventType), + attributes.map( + FileAttributes.fromFileSystemAttributes(root, eventPath, _) + ) ) + } } -/** Type of a file event. - */ +/** Type of a file event. */ sealed trait FileEventKind extends EnumEntry object FileEventKind extends Enum[FileEventKind] with CirceEnum[FileEventKind] { 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 de160083918..835b67d8106 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 @@ -183,7 +183,11 @@ object FileManagerApi { case object EventFile extends Method("file/event") { - case class Params(path: Path, kind: FileEventKind) + case class Params( + path: Path, + kind: FileEventKind, + attributes: Option[FileAttributes] + ) implicit val hasParams: HasParams.Aux[this.type, EventFile.Params] = new HasParams[this.type] { diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/PathWatcher.scala b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/PathWatcher.scala index 0e9928aac76..5d1617d4474 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/PathWatcher.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/filemanager/PathWatcher.scala @@ -140,7 +140,17 @@ final class PathWatcher( case e: Watcher.WatcherEvent => restartCounter.reset() - val event = FileEvent.fromWatcherEvent(root, base, e) + + val fileInfo = + if (e.eventType == Watcher.EventTypeDelete) ZIO.fail(FileNotFound) + else fs.info(e.path.toFile) + + exec + .exec(fileInfo) + .map(FileEvent.fromWatcherEvent(root, base, e, _)) + .pipeTo(self) + + case event: FileEvent => clients.foreach(_ ! FileEventResult(event)) context.system.eventStream.publish(event) diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala index edaf80729b8..98b2b23c1d6 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/protocol/json/JsonConnectionController.scala @@ -354,13 +354,16 @@ class JsonConnectionController( FileModifiedOnDisk.Params(path) ) - case TextProtocol.FileEvent(path, event) => - webActor ! Notification(EventFile, EventFile.Params(path, event)) + case TextProtocol.FileEvent(path, event, attributes) => + webActor ! Notification( + EventFile, + EventFile.Params(path, event, attributes) + ) case PathWatcherProtocol.FileEventResult(event) => webActor ! Notification( EventFile, - EventFile.Params(event.path, event.kind) + EventFile.Params(event.path, event.kind, event.attributes.toOption) ) case ContextRegistryProtocol diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/text/BufferRegistry.scala b/engine/language-server/src/main/scala/org/enso/languageserver/text/BufferRegistry.scala index fa8cb9ea353..169b43d950b 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/text/BufferRegistry.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/text/BufferRegistry.scala @@ -215,7 +215,7 @@ class BufferRegistry( registry ) - case msg @ FileEvent(path, kind) => + case msg @ FileEvent(path, kind, _) => if (kind == FileEventKind.Added || kind == FileEventKind.Modified) { registry.get(path).foreach { buffer => buffer ! msg diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/text/CollaborativeBuffer.scala b/engine/language-server/src/main/scala/org/enso/languageserver/text/CollaborativeBuffer.scala index cecfea3fafe..5f908af5cee 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/text/CollaborativeBuffer.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/text/CollaborativeBuffer.scala @@ -324,81 +324,37 @@ class CollaborativeBuffer( ) ) - case FileEvent(path, _) => - fileManager ! FileManagerProtocol.InfoFile(path) - val timeoutCancellable = context.system.scheduler.scheduleOnce( - timingsConfig.requestTimeout, - self, - IOTimeout - ) - context.become( - waitingOnFileEventContent( - path, - buffer, - timeoutCancellable, - clients, - lockHolder, - autoSave - ) - ) - - } - - private def waitingOnFileEventContent( - path: Path, - buffer: Buffer, - timeoutCancellable: Cancellable, - clients: Map[ClientId, JsonSession], - lockHolder: Option[JsonSession], - autoSave: Map[ClientId, (ContentVersion, Cancellable)] - ): Receive = { - case FileManagerProtocol.InfoFileResult(Right(attrs)) => - timeoutCancellable.cancel() - val newBuffer = buffer.fileWithMetadata.lastModifiedTime.map { - bufferLastModifiedTime => - if (attrs.lastModifiedTime.isAfter(bufferLastModifiedTime)) { - clients.values.foreach { - _.rpcController ! FileModifiedOnDisk(path) - } - buffer - .withLastModifiedTime(attrs.lastModifiedTime) - .withModifiedOnDisk() - } else { - buffer + case FileEvent(path, _, attributes) => + attributes match { + case Right(attrs) => + val newBuffer = buffer.fileWithMetadata.lastModifiedTime.map { + bufferLastModifiedTime => + if (attrs.lastModifiedTime.isAfter(bufferLastModifiedTime)) { + clients.values.foreach { + _.rpcController ! FileModifiedOnDisk(path) + } + buffer + .withLastModifiedTime(attrs.lastModifiedTime) + .withModifiedOnDisk() + } else { + buffer + } } + context.become( + collaborativeEditing( + newBuffer.getOrElse(buffer), + clients, + lockHolder, + autoSave + ) + ) + case Left(failure) => + logger.error( + "Failed to read file attributes for [{}]. {}", + path, + failure + ) } - unstashAll() - context.become( - collaborativeEditing( - newBuffer.getOrElse(buffer), - clients, - lockHolder, - autoSave - ) - ) - - case FileManagerProtocol.InfoFileResult(Left(err)) => - timeoutCancellable.cancel() - logger.error("Failed to read file attributes for [{}]. {}", path, err) - unstashAll() - context.become( - collaborativeEditing(buffer, clients, lockHolder, autoSave) - ) - - case Status.Failure(ex) => - logger.error("Failed to read file attributes for [{}].", path, ex) - unstashAll() - context.become( - collaborativeEditing(buffer, clients, lockHolder, autoSave) - ) - - case IOTimeout => - unstashAll() - context.become( - collaborativeEditing(buffer, clients, lockHolder, autoSave) - ) - - case _ => stash() } private def waitingOnReloadedContent( @@ -444,7 +400,11 @@ class CollaborativeBuffer( case FileManagerProtocol.ReadFileWithAttributesResult(Left(FileNotFound)) => clients.values.foreach { - _.rpcController ! TextProtocol.FileEvent(path, FileEventKind.Removed) + _.rpcController ! TextProtocol.FileEvent( + path, + FileEventKind.Removed, + None + ) } replyTo ! ReloadedBuffer(path) timeoutCancellable.cancel() diff --git a/engine/language-server/src/main/scala/org/enso/languageserver/text/TextProtocol.scala b/engine/language-server/src/main/scala/org/enso/languageserver/text/TextProtocol.scala index 08696ae5730..affd09aafe0 100644 --- a/engine/language-server/src/main/scala/org/enso/languageserver/text/TextProtocol.scala +++ b/engine/language-server/src/main/scala/org/enso/languageserver/text/TextProtocol.scala @@ -2,6 +2,7 @@ package org.enso.languageserver.text import org.enso.languageserver.data.{CapabilityRegistration, ClientId} import org.enso.languageserver.filemanager.{ + FileAttributes, FileEventKind, FileSystemFailure, Path @@ -151,8 +152,14 @@ object TextProtocol { * a file event after reloading the buffer to sync with file system * * @param path path to the file + * @param kind file event kind + * @param attributes file attributes */ - case class FileEvent(path: Path, event: FileEventKind) + case class FileEvent( + path: Path, + kind: FileEventKind, + attributes: Option[FileAttributes] + ) /** Requests the language server to save a file on behalf of a given user. * diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ReceivesTreeUpdatesHandlerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ReceivesTreeUpdatesHandlerTest.scala index f54d7bf8cab..42438f7cfe7 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ReceivesTreeUpdatesHandlerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/ReceivesTreeUpdatesHandlerTest.scala @@ -84,7 +84,7 @@ class ReceivesTreeUpdatesHandlerTest // create file val path = Paths.get(testContentRoot.file.toString, "oneone.txt") Files.createFile(path) - client1.expectJson(json""" + client1.fuzzyExpectJson(json""" { "jsonrpc": "2.0", "method": "file/event", "params": { @@ -92,11 +92,12 @@ class ReceivesTreeUpdatesHandlerTest "rootId": $testContentRootId, "segments": [ "oneone.txt" ] }, - "kind": "Added" + "kind": "Added", + "attributes": "*" } } """) - client2.expectJson(json""" + client2.fuzzyExpectJson(json""" { "jsonrpc": "2.0", "method": "file/event", "params": { @@ -104,14 +105,15 @@ class ReceivesTreeUpdatesHandlerTest "rootId": $testContentRootId, "segments": [ "oneone.txt" ] }, - "kind": "Added" + "kind": "Added", + "attributes": "*" } } """) // update file Files.write(path, "Hello".getBytes()) - client1.expectJson(json""" + client1.fuzzyExpectJson(json""" { "jsonrpc": "2.0", "method": "file/event", "params": { @@ -119,11 +121,12 @@ class ReceivesTreeUpdatesHandlerTest "rootId": $testContentRootId, "segments": [ "oneone.txt" ] }, - "kind": "Modified" + "kind": "Modified", + "attributes": "*" } } """) - client2.expectJson(json""" + client2.fuzzyExpectJson(json""" { "jsonrpc": "2.0", "method": "file/event", "params": { @@ -131,7 +134,8 @@ class ReceivesTreeUpdatesHandlerTest "rootId": $testContentRootId, "segments": [ "oneone.txt" ] }, - "kind": "Modified" + "kind": "Modified", + "attributes": "*" } } """) @@ -150,7 +154,8 @@ class ReceivesTreeUpdatesHandlerTest "rootId": $testContentRootId, "segments": [ "oneone.txt" ] }, - "kind": "Removed" + "kind": "Removed", + "attributes": null } } """) diff --git a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VcsManagerTest.scala b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VcsManagerTest.scala index 0225da08584..0c474f50b2e 100644 --- a/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VcsManagerTest.scala +++ b/engine/language-server/src/test/scala/org/enso/languageserver/websocket/json/VcsManagerTest.scala @@ -1324,7 +1324,8 @@ class VcsManagerTest $testBarFileName ] }, - "kind" : "Removed" + "kind" : "Removed", + "attributes" : null } } """ @@ -1354,7 +1355,8 @@ class VcsManagerTest $testBarFileName ] }, - "kind" : "Removed" + "kind" : "Removed", + "attributes" : null } } """)