Add attributes to filesystem events (#8767)

close #8200

Changelog:
- add: `attributes` field to the file event notification
This commit is contained in:
Dmitry Bushev 2024-01-16 13:48:53 +00:00 committed by GitHub
parent 04b0d266e8
commit 51540e2eb2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 102 additions and 99 deletions

View File

@ -2350,6 +2350,7 @@ of the (possibly multiple) content roots.
interface FileEventNotification { interface FileEventNotification {
path: Path; path: Path;
kind: FileEventKind; kind: FileEventKind;
attributes?: FileAttributes;
} }
``` ```

View File

@ -8,8 +8,13 @@ import org.enso.filewatcher.Watcher
* *
* @param path path to the file system object * @param path path to the file system object
* @param kind type of file system event * @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 { object FileEvent {
@ -18,21 +23,27 @@ object FileEvent {
* @param root a project root * @param root a project root
* @param base a watched path * @param base a watched path
* @param event a file system event * @param event a file system event
* @param attributes a file attributes
* @return file event * @return file event
*/ */
def fromWatcherEvent( def fromWatcherEvent(
root: File, root: File,
base: Path, base: Path,
event: Watcher.WatcherEvent event: Watcher.WatcherEvent,
): FileEvent = attributes: Either[FileSystemFailure, FileSystemApi.Attributes]
): FileEvent = {
val eventPath = Path.getRelativePath(root, base, event.path)
FileEvent( FileEvent(
Path.getRelativePath(root, base, event.path), eventPath,
FileEventKind(event.eventType) FileEventKind(event.eventType),
attributes.map(
FileAttributes.fromFileSystemAttributes(root, eventPath, _)
)
) )
}
} }
/** Type of a file event. /** Type of a file event. */
*/
sealed trait FileEventKind extends EnumEntry sealed trait FileEventKind extends EnumEntry
object FileEventKind extends Enum[FileEventKind] with CirceEnum[FileEventKind] { object FileEventKind extends Enum[FileEventKind] with CirceEnum[FileEventKind] {

View File

@ -183,7 +183,11 @@ object FileManagerApi {
case object EventFile extends Method("file/event") { 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] = implicit val hasParams: HasParams.Aux[this.type, EventFile.Params] =
new HasParams[this.type] { new HasParams[this.type] {

View File

@ -140,7 +140,17 @@ final class PathWatcher(
case e: Watcher.WatcherEvent => case e: Watcher.WatcherEvent =>
restartCounter.reset() 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)) clients.foreach(_ ! FileEventResult(event))
context.system.eventStream.publish(event) context.system.eventStream.publish(event)

View File

@ -354,13 +354,16 @@ class JsonConnectionController(
FileModifiedOnDisk.Params(path) FileModifiedOnDisk.Params(path)
) )
case TextProtocol.FileEvent(path, event) => case TextProtocol.FileEvent(path, event, attributes) =>
webActor ! Notification(EventFile, EventFile.Params(path, event)) webActor ! Notification(
EventFile,
EventFile.Params(path, event, attributes)
)
case PathWatcherProtocol.FileEventResult(event) => case PathWatcherProtocol.FileEventResult(event) =>
webActor ! Notification( webActor ! Notification(
EventFile, EventFile,
EventFile.Params(event.path, event.kind) EventFile.Params(event.path, event.kind, event.attributes.toOption)
) )
case ContextRegistryProtocol case ContextRegistryProtocol

View File

@ -215,7 +215,7 @@ class BufferRegistry(
registry registry
) )
case msg @ FileEvent(path, kind) => case msg @ FileEvent(path, kind, _) =>
if (kind == FileEventKind.Added || kind == FileEventKind.Modified) { if (kind == FileEventKind.Added || kind == FileEventKind.Modified) {
registry.get(path).foreach { buffer => registry.get(path).foreach { buffer =>
buffer ! msg buffer ! msg

View File

@ -324,81 +324,37 @@ class CollaborativeBuffer(
) )
) )
case FileEvent(path, _) => case FileEvent(path, _, attributes) =>
fileManager ! FileManagerProtocol.InfoFile(path) attributes match {
val timeoutCancellable = context.system.scheduler.scheduleOnce( case Right(attrs) =>
timingsConfig.requestTimeout, val newBuffer = buffer.fileWithMetadata.lastModifiedTime.map {
self, bufferLastModifiedTime =>
IOTimeout if (attrs.lastModifiedTime.isAfter(bufferLastModifiedTime)) {
) clients.values.foreach {
context.become( _.rpcController ! FileModifiedOnDisk(path)
waitingOnFileEventContent( }
path, buffer
buffer, .withLastModifiedTime(attrs.lastModifiedTime)
timeoutCancellable, .withModifiedOnDisk()
clients, } else {
lockHolder, buffer
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
} }
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( private def waitingOnReloadedContent(
@ -444,7 +400,11 @@ class CollaborativeBuffer(
case FileManagerProtocol.ReadFileWithAttributesResult(Left(FileNotFound)) => case FileManagerProtocol.ReadFileWithAttributesResult(Left(FileNotFound)) =>
clients.values.foreach { clients.values.foreach {
_.rpcController ! TextProtocol.FileEvent(path, FileEventKind.Removed) _.rpcController ! TextProtocol.FileEvent(
path,
FileEventKind.Removed,
None
)
} }
replyTo ! ReloadedBuffer(path) replyTo ! ReloadedBuffer(path)
timeoutCancellable.cancel() timeoutCancellable.cancel()

View File

@ -2,6 +2,7 @@ package org.enso.languageserver.text
import org.enso.languageserver.data.{CapabilityRegistration, ClientId} import org.enso.languageserver.data.{CapabilityRegistration, ClientId}
import org.enso.languageserver.filemanager.{ import org.enso.languageserver.filemanager.{
FileAttributes,
FileEventKind, FileEventKind,
FileSystemFailure, FileSystemFailure,
Path Path
@ -151,8 +152,14 @@ object TextProtocol {
* a file event after reloading the buffer to sync with file system * a file event after reloading the buffer to sync with file system
* *
* @param path path to the file * @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. /** Requests the language server to save a file on behalf of a given user.
* *

View File

@ -84,7 +84,7 @@ class ReceivesTreeUpdatesHandlerTest
// create file // create file
val path = Paths.get(testContentRoot.file.toString, "oneone.txt") val path = Paths.get(testContentRoot.file.toString, "oneone.txt")
Files.createFile(path) Files.createFile(path)
client1.expectJson(json""" client1.fuzzyExpectJson(json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"method": "file/event", "method": "file/event",
"params": { "params": {
@ -92,11 +92,12 @@ class ReceivesTreeUpdatesHandlerTest
"rootId": $testContentRootId, "rootId": $testContentRootId,
"segments": [ "oneone.txt" ] "segments": [ "oneone.txt" ]
}, },
"kind": "Added" "kind": "Added",
"attributes": "*"
} }
} }
""") """)
client2.expectJson(json""" client2.fuzzyExpectJson(json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"method": "file/event", "method": "file/event",
"params": { "params": {
@ -104,14 +105,15 @@ class ReceivesTreeUpdatesHandlerTest
"rootId": $testContentRootId, "rootId": $testContentRootId,
"segments": [ "oneone.txt" ] "segments": [ "oneone.txt" ]
}, },
"kind": "Added" "kind": "Added",
"attributes": "*"
} }
} }
""") """)
// update file // update file
Files.write(path, "Hello".getBytes()) Files.write(path, "Hello".getBytes())
client1.expectJson(json""" client1.fuzzyExpectJson(json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"method": "file/event", "method": "file/event",
"params": { "params": {
@ -119,11 +121,12 @@ class ReceivesTreeUpdatesHandlerTest
"rootId": $testContentRootId, "rootId": $testContentRootId,
"segments": [ "oneone.txt" ] "segments": [ "oneone.txt" ]
}, },
"kind": "Modified" "kind": "Modified",
"attributes": "*"
} }
} }
""") """)
client2.expectJson(json""" client2.fuzzyExpectJson(json"""
{ "jsonrpc": "2.0", { "jsonrpc": "2.0",
"method": "file/event", "method": "file/event",
"params": { "params": {
@ -131,7 +134,8 @@ class ReceivesTreeUpdatesHandlerTest
"rootId": $testContentRootId, "rootId": $testContentRootId,
"segments": [ "oneone.txt" ] "segments": [ "oneone.txt" ]
}, },
"kind": "Modified" "kind": "Modified",
"attributes": "*"
} }
} }
""") """)
@ -150,7 +154,8 @@ class ReceivesTreeUpdatesHandlerTest
"rootId": $testContentRootId, "rootId": $testContentRootId,
"segments": [ "oneone.txt" ] "segments": [ "oneone.txt" ]
}, },
"kind": "Removed" "kind": "Removed",
"attributes": null
} }
} }
""") """)

View File

@ -1324,7 +1324,8 @@ class VcsManagerTest
$testBarFileName $testBarFileName
] ]
}, },
"kind" : "Removed" "kind" : "Removed",
"attributes" : null
} }
} }
""" """
@ -1354,7 +1355,8 @@ class VcsManagerTest
$testBarFileName $testBarFileName
] ]
}, },
"kind" : "Removed" "kind" : "Removed",
"attributes" : null
} }
} }
""") """)