Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sync file system and language server after restore #4020

Merged
merged 6 commits into from
Jan 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@
- [Simplify exception handling for polyglot exceptions][3981]
- [Simplify compilation of nested patterns][4005]
- [IGV can jump to JMH sources & more][4008]
- [Sync language server with file system after VCS restore][4020]

[3227]: https://github.com/enso-org/enso/pull/3227
[3248]: https://github.com/enso-org/enso/pull/3248
Expand Down Expand Up @@ -575,6 +576,7 @@
[3981]: https://github.com/enso-org/enso/pull/3981
[4005]: https://github.com/enso-org/enso/pull/4005
[4008]: https://github.com/enso-org/enso/pull/4008
[4020]: https://github.com/enso-org/enso/pull/4020

# Enso 2.0.0-alpha.18 (2021-10-12)

Expand Down
17 changes: 15 additions & 2 deletions docs/language-server/protocol-language-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -2804,6 +2804,17 @@ checkpoint recorded with `vcs/save`. If no save exists with a provided
restore the project to the last saved state, will all current modifications
forgotten.

If the contents of any open buffer has changed as a result of this operation,
all subscribed clients will be notified about the new version of the file via
`text/didChange` push notification.

A file might have been removed during the operation while there were still open
buffers for that file. Any such clients will be modified of a file removal via
the `file/event` notification.

The result of the call returns a list of files that have been modified during
the operation.

#### Parameters

```typescript
Expand All @@ -2826,7 +2837,9 @@ forgotten.
#### Result

```typescript
null;
{
changed: [Path];
}
```

### `vcs/list`
Expand Down Expand Up @@ -3485,7 +3498,7 @@ on the stack. In general, all consequent stack items should be `LocalCall`s.
}
```

Returns successful reponse.
Returns successful response.

```json
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,9 @@ class JsonConnectionController(
case TextProtocol.FileAutoSaved(path) =>
webActor ! Notification(FileAutoSaved, FileAutoSaved.Params(path))

case TextProtocol.FileEvent(path, event) =>
webActor ! Notification(EventFile, EventFile.Params(path, event))

case PathWatcherProtocol.FileEventResult(event) =>
webActor ! Notification(
EventFile,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ class RestoreVcsHandler(
replyTo ! ResponseError(Some(id), Errors.RequestTimeout)
context.stop(self)

case VcsProtocol.RestoreRepoResponse(Right(_)) =>
replyTo ! ResponseResult(RestoreVcs, id, Unused)
case VcsProtocol.RestoreRepoResponse(Right(paths)) =>
replyTo ! ResponseResult(RestoreVcs, id, RestoreVcs.Result(paths))
cancellable.cancel()
context.stop(self)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
package org.enso.languageserver.text

import java.io.File

import org.enso.text.{ContentBasedVersioning, ContentVersion}
import org.enso.text.buffer.Rope
import org.enso.text.editing.model.Position
import org.enso.text.editing.model.Range

/** A buffer state representation.
*
Expand All @@ -17,7 +18,18 @@ case class Buffer(
contents: Rope,
inMemory: Boolean,
version: ContentVersion
)
) {

/** Returns a range covering the whole buffer.
*/
lazy val fullRange: Range = {
val lines = contents.lines.length
Range(
Position(0, 0),
Position(lines - 1, contents.lines.drop(lines - 1).characters.length)
)
}
}

object Buffer {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,18 @@ import org.enso.languageserver.data.{CanEdit, CapabilityRegistration, ClientId}
import org.enso.languageserver.event.InitializedEvent
import org.enso.languageserver.filemanager.Path
import org.enso.languageserver.monitoring.MonitoringProtocol.{Ping, Pong}
import org.enso.languageserver.text.BufferRegistry.SaveTimeout
import org.enso.languageserver.text.CollaborativeBuffer.ForceSave
import org.enso.languageserver.session.JsonSession
import org.enso.languageserver.text.BufferRegistry.{
ReloadBufferTimeout,
SaveTimeout,
VcsTimeout
}
import org.enso.languageserver.text.CollaborativeBuffer.{
ForceSave,
ReloadBuffer,
ReloadBufferFailed,
ReloadedBuffer
}
import org.enso.languageserver.util.UnhandledLogging
import org.enso.languageserver.text.TextProtocol.{
ApplyEdit,
Expand All @@ -27,9 +37,11 @@ import org.enso.languageserver.text.TextProtocol.{
SaveFailed,
SaveFile
}
import org.enso.languageserver.vcsmanager.GenericVcsFailure
import org.enso.languageserver.vcsmanager.VcsProtocol.{
InitRepo,
RestoreRepo,
RestoreRepoResponse,
SaveRepo
}
import org.enso.text.ContentBasedVersioning
Expand Down Expand Up @@ -192,20 +204,25 @@ class BufferRegistry(
}

case msg @ InitRepo(clientId, path) =>
waitOnVCSActionToComplete((msg, sender()), clientId, registry, path)
forwardMessageToVCS((msg, sender()), clientId, path, registry)

case msg @ SaveRepo(clientId, path, _) =>
waitOnVCSActionToComplete((msg, sender()), clientId, registry, path)
forwardMessageToVCS((msg, sender()), clientId, path, registry)

case msg @ RestoreRepo(clientId, path, _) =>
waitOnVCSActionToComplete((msg, sender()), clientId, registry, path)
forwardMessageToVCSAndReloadBuffers(
(msg, sender()),
clientId,
path,
registry
)
}

private def waitOnVCSActionToComplete(
private def forwardMessageToVCS(
msgWithSender: (Any, ActorRef),
clientId: ClientId,
registry: Map[Path, ActorRef],
root: Path
root: Path,
registry: Map[Path, ActorRef]
): Unit = {
val openBuffers = registry.filter(_._1.startsWith(root))
val timeouts = openBuffers.map { case (_, actorRef) =>
Expand All @@ -232,6 +249,7 @@ class BufferRegistry(
timeouts: Map[ActorRef, Cancellable]
): Receive = {
case SaveTimeout(from) =>
// TODO: log failure
val timeouts1 = timeouts.removed(from)
if (timeouts1.isEmpty) {
vcsManager.tell(msg._1, msg._2)
Expand All @@ -243,6 +261,7 @@ class BufferRegistry(
)
}
case SaveFailed | FileSaved =>
// TODO: log failure
timeouts.get(sender()).foreach(_.cancel())
val timeouts1 = timeouts.removed(sender())
if (timeouts1.isEmpty) {
Expand All @@ -258,12 +277,203 @@ class BufferRegistry(
stash()
}

private def forwardMessageToVCSAndReloadBuffers(
msgWithSender: (Any, ActorRef),
clientId: ClientId,
root: Path,
registry: Map[Path, ActorRef]
): Unit = {
val openBuffers = registry.filter(_._1.startsWith(root))
val timeouts = openBuffers.map { case (_, actorRef) =>
actorRef ! ForceSave(clientId)
(
actorRef,
context.system.scheduler
.scheduleOnce(timingsConfig.requestTimeout, self, SaveTimeout)
)
}
if (timeouts.isEmpty) {
vcsManager.tell(msgWithSender._1, self)
val timeout = context.system.scheduler
.scheduleOnce(timingsConfig.requestTimeout, self, VcsTimeout)
context.become(
waitOnVcsRestoreResponse(clientId, msgWithSender._2, timeout, registry)
)
} else {
context.become(
waitOnSaveConfirmationForwardToVCSAndReload(
clientId,
msgWithSender,
registry,
timeouts
)
)
}
}

private def waitOnSaveConfirmationForwardToVCSAndReload(
clientId: ClientId,
msg: (Any, ActorRef),
registry: Map[Path, ActorRef],
timeouts: Map[ActorRef, Cancellable]
): Receive = {
case SaveTimeout(from) =>
val timeouts1 = timeouts.removed(from)
if (timeouts1.isEmpty) {
vcsManager ! msg._1
val vcsTimeout = context.system.scheduler
.scheduleOnce(timingsConfig.requestTimeout, self, SaveTimeout)
unstashAll()
context.become(
waitOnVcsRestoreResponse(clientId, msg._2, vcsTimeout, registry)
)
} else {
context.become(
waitOnSaveConfirmationForwardToVCSAndReload(
clientId,
msg,
registry,
timeouts1
)
)
}

case SaveFailed | FileSaved =>
timeouts.get(sender()).foreach(_.cancel())
val timeouts1 = timeouts.removed(sender())
if (timeouts1.isEmpty) {
vcsManager ! msg._1
val vcsTimeout = context.system.scheduler
.scheduleOnce(timingsConfig.requestTimeout, self, VcsTimeout)
unstashAll()
context.become(
waitOnVcsRestoreResponse(clientId, msg._2, vcsTimeout, registry)
)
} else {
context.become(
waitOnSaveConfirmationForwardToVCSAndReload(
clientId,
msg,
registry,
timeouts1
)
)
}

case _ =>
stash()
}

private def waitOnVcsRestoreResponse(
clientId: ClientId,
sender: ActorRef,
timeout: Cancellable,
registry: Map[Path, ActorRef]
): Receive = {
case response @ RestoreRepoResponse(Right(_)) =>
if (timeout != null) timeout.cancel()
reloadBuffers(clientId, sender, response, registry)

case response @ RestoreRepoResponse(Left(_)) =>
if (timeout != null) timeout.cancel()
sender ! response
unstashAll()
context.become(running(registry))

case VcsTimeout =>
sender ! RestoreRepoResponse(Left(GenericVcsFailure("operation timeout")))
unstashAll()
context.become(running(registry))

case _ =>
stash()
}

private def reloadBuffers(
clientId: ClientId,
from: ActorRef,
response: RestoreRepoResponse,
registry: Map[Path, ActorRef]
): Unit = {
val filesDiff = response.result.getOrElse(Nil)
val timeouts = registry.filter(r => filesDiff.contains(r._1)).map {
case (path, collaborativeEditor) =>
collaborativeEditor ! ReloadBuffer(JsonSession(clientId, from), path)
(
path,
context.system.scheduler
.scheduleOnce(
timingsConfig.requestTimeout,
self,
ReloadBufferTimeout(path)
)
)
}

if (timeouts.isEmpty) {
from ! response
context.become(running(registry))
} else {
context.become(
waitingOnBuffersToReload(from, timeouts, registry, response)
)
}
}

private def waitingOnBuffersToReload(
from: ActorRef,
timeouts: Map[Path, Cancellable],
registry: Map[Path, ActorRef],
response: RestoreRepoResponse
): Receive = {
case ReloadedBuffer(path) =>
timeouts.get(path).foreach(_.cancel())
val timeouts1 = timeouts.removed(path)
if (timeouts1.isEmpty) {
from ! response
context.become(running(registry))
} else {
context.become(
waitingOnBuffersToReload(from, timeouts1, registry, response)
)
}

case ReloadBufferFailed(path, _) =>
timeouts.get(path).foreach(_.cancel())
val timeouts1 = timeouts.removed(path)
if (timeouts1.isEmpty) {
// TODO: log failure
from ! response
context.become(running(registry))
} else {
context.become(
waitingOnBuffersToReload(from, timeouts1, registry, response)
)
}

case ReloadBufferTimeout(path) =>
val timeouts1 = timeouts.removed(path)
if (timeouts1.isEmpty) {
// TODO: log failure
from ! response
context.become(running(registry))
} else {
context.become(
waitingOnBuffersToReload(from, timeouts1, registry, response)
)
}
}

}

object BufferRegistry {

case class SaveTimeout(ref: ActorRef)

case class ReloadBufferTimeout(path: Path)

case object VcsTimeout

/** Creates a configuration object used to create a [[BufferRegistry]]
*
* @param fileManager a file manager actor
Expand Down
Loading