Files
EVEngine/src/modules/editor/EditorDocumentService.cpp
T

221 lines
12 KiB
C++

#include "editor/EditorDocumentService.h"
#include "editor/EditorValueJson.h"
#include <algorithm>
namespace eve::editor {
EditorResult<StoredDocument> MemoryAtomicDocumentStore::read(const std::string& resourceUri) const {
auto found = documents_.find(resourceUri);
if (found == documents_.end())
return EditorResult<StoredDocument>::error(EditorStatus::NotFound, RuleId("editor.document.store-not-found"),
"Document resource is not persisted: " + resourceUri);
return EditorResult<StoredDocument>::applied(found->second);
}
EditorResult<StoredDocument> MemoryAtomicDocumentStore::compareAndSwap(const std::string& resourceUri,
Revision expectedRevision,
const std::string& expectedContentHash,
const EditorValue& content) {
auto found = documents_.find(resourceUri);
const Revision current = found == documents_.end() ? 0 : found->second.revision;
const std::string currentHash = found == documents_.end() ? std::string{} : found->second.contentHash;
if (current != expectedRevision || currentHash != expectedContentHash)
return EditorResult<StoredDocument>::error(EditorStatus::Conflict, RuleId("editor.document.disk-conflict"),
"Document store revision changed before save");
StoredDocument stored;
stored.content = content;
stored.revision = current + 1;
stored.contentHash = editorValueContentHash(content);
documents_.insert_or_assign(resourceUri, stored);
return EditorResult<StoredDocument>::applied(std::move(stored));
}
EditorResult<DocumentSnapshot> DocumentService::open(DocumentKey key, std::string title, std::string resourceUri,
EditorValue initialContent) {
if (!store_ || key.asset.empty() || resourceUri.empty())
return error(EditorStatus::Rejected, "editor.document.invalid-open",
"Document store, asset identity and resource URI are required");
DocumentId id("document:" + key.asset.value());
if (open_.contains(id)) return EditorResult<DocumentSnapshot>::applied(open_.at(id).snapshot);
OpenDocument document;
document.snapshot.id = id;
document.snapshot.key = std::move(key);
document.snapshot.title = std::move(title);
document.snapshot.resourceUri = std::move(resourceUri);
document.snapshot.state = DocumentState::Ready;
EditorResult<StoredDocument> stored = store_->read(document.snapshot.resourceUri);
if (stored.isAccepted() && stored.value) {
document.content = stored.value->content;
document.snapshot.revision.disk = stored.value->revision;
document.snapshot.revision.edit = stored.value->revision;
document.snapshot.revision.saved = stored.value->revision;
document.snapshot.diskContentHash = stored.value->contentHash;
} else if (stored.status == EditorStatus::NotFound) {
document.content = std::move(initialContent);
} else {
return error(stored.status, "editor.document.store-read", "Document store could not be read");
}
DocumentSnapshot snapshot = document.snapshot;
open_.emplace(id, std::move(document));
return EditorResult<DocumentSnapshot>::applied(std::move(snapshot));
}
EditorResult<DocumentSnapshot> DocumentService::edit(const DocumentId& document, EditorValue content,
std::optional<Revision> expectedRevision) {
auto found = open_.find(document);
if (found == open_.end()) return error(EditorStatus::NotFound, "editor.document.not-open", "Document is not open");
if (expectedRevision && *expectedRevision != found->second.snapshot.revision.edit)
return error(EditorStatus::Conflict, "editor.document.edit-conflict",
"Document edit revision changed before mutation");
found->second.content = std::move(content);
++found->second.snapshot.revision.edit;
found->second.snapshot.state = DocumentState::Ready;
return EditorResult<DocumentSnapshot>::applied(found->second.snapshot);
}
EditorResult<SaveTicket> DocumentService::requestSave(const DocumentId& document) {
auto found = open_.find(document);
if (found == open_.end())
return EditorResult<SaveTicket>::error(EditorStatus::NotFound, RuleId("editor.document.not-open"),
"Document is not open");
SaveTicket ticket;
ticket.id = StableId(document.value() + ".save." + std::to_string(++ticketSequence_));
ticket.document = document;
ticket.capturedEditRevision = found->second.snapshot.revision.edit;
ticket.expectedDiskRevision = found->second.snapshot.revision.disk;
ticket.expectedContentHash = found->second.snapshot.diskContentHash;
pendingSaves_.emplace(ticket.id, PendingSave{ticket, found->second.content});
return EditorResult<SaveTicket>::applied(std::move(ticket));
}
EditorResult<DocumentSnapshot> DocumentService::executeSave(const SaveTicket& ticket) {
auto pending = pendingSaves_.find(ticket.id);
auto document = open_.find(ticket.document);
if (pending == pendingSaves_.end() || document == open_.end())
return error(EditorStatus::NotFound, "editor.document.save-ticket-not-found",
"Save ticket or document is no longer available");
document->second.snapshot.state = DocumentState::Saving;
EditorResult<StoredDocument> stored =
store_->compareAndSwap(document->second.snapshot.resourceUri, ticket.expectedDiskRevision,
ticket.expectedContentHash, pending->second.content);
pendingSaves_.erase(pending);
if (!stored.isAccepted() || !stored.value) {
document->second.snapshot.state =
stored.status == EditorStatus::Conflict ? DocumentState::Conflict : DocumentState::Failed;
document->second.snapshot.diagnostics = stored.diagnostics;
EditorResult<DocumentSnapshot> result;
result.status = stored.status;
result.value = document->second.snapshot;
result.diagnostics = std::move(stored.diagnostics);
return result;
}
document->second.snapshot.revision.disk = stored.value->revision;
document->second.snapshot.revision.saved = ticket.capturedEditRevision;
document->second.snapshot.diskContentHash = stored.value->contentHash;
document->second.snapshot.state = DocumentState::Ready;
document->second.snapshot.diagnostics.clear();
return EditorResult<DocumentSnapshot>::applied(document->second.snapshot);
}
EditorResult<EditorValue> DocumentService::content(const DocumentId& document) const {
auto found = open_.find(document);
if (found == open_.end())
return EditorResult<EditorValue>::error(EditorStatus::NotFound, RuleId("editor.document.not-open"),
"Document is not open");
return EditorResult<EditorValue>::applied(found->second.content);
}
EditorResult<DocumentSnapshot> DocumentService::snapshot(const DocumentId& document) const {
auto found = open_.find(document);
if (found == open_.end()) return error(EditorStatus::NotFound, "editor.document.not-open", "Document is not open");
return EditorResult<DocumentSnapshot>::applied(found->second.snapshot);
}
std::vector<DocumentSnapshot> DocumentService::documents() const {
std::vector<DocumentSnapshot> result;
result.reserve(open_.size());
for (const auto& [id, document] : open_) {
(void)id;
result.push_back(document.snapshot);
}
std::sort(result.begin(), result.end(),
[](const DocumentSnapshot& left, const DocumentSnapshot& right) { return left.id < right.id; });
return result;
}
EditorResult<DocumentSnapshot> DocumentService::reconcileExternal(const DocumentId& document) {
auto found = open_.find(document);
if (found == open_.end()) return error(EditorStatus::NotFound, "editor.document.not-open", "Document is not open");
EditorResult<StoredDocument> stored = store_->read(found->second.snapshot.resourceUri);
if (!stored.isAccepted() || !stored.value) {
const bool expectedMissing = stored.status == EditorStatus::NotFound &&
found->second.snapshot.revision.disk == 0 &&
found->second.snapshot.diskContentHash.empty();
if (expectedMissing) return EditorResult<DocumentSnapshot>::applied(found->second.snapshot);
found->second.snapshot.state = stored.status == EditorStatus::NotFound ||
stored.status == EditorStatus::Conflict
? DocumentState::Conflict
: DocumentState::Failed;
found->second.snapshot.diagnostics = stored.diagnostics;
if (found->second.snapshot.diagnostics.empty())
found->second.snapshot.diagnostics.push_back(
{RuleId("editor.document.external-missing"), DiagnosticSeverity::Error,
"Persisted document was removed externally"});
EditorResult<DocumentSnapshot> result;
result.status = found->second.snapshot.state == DocumentState::Conflict ? EditorStatus::Conflict
: stored.status;
result.value = found->second.snapshot;
result.diagnostics = found->second.snapshot.diagnostics;
return result;
}
const bool changed = stored.value->revision != found->second.snapshot.revision.disk ||
stored.value->contentHash != found->second.snapshot.diskContentHash;
if (!changed) {
if (found->second.snapshot.state == DocumentState::Conflict) {
found->second.snapshot.state = DocumentState::Ready;
found->second.snapshot.diagnostics.clear();
}
return EditorResult<DocumentSnapshot>::applied(found->second.snapshot);
}
if (found->second.snapshot.dirty()) {
found->second.snapshot.state = DocumentState::Conflict;
found->second.snapshot.diagnostics = {
{RuleId("editor.document.external-conflict"), DiagnosticSeverity::Error,
"Document changed on disk while the session has unsaved edits"}};
EditorResult<DocumentSnapshot> result;
result.status = EditorStatus::Conflict;
result.value = found->second.snapshot;
result.diagnostics = found->second.snapshot.diagnostics;
return result;
}
found->second.content = stored.value->content;
const Revision synchronizedEdit =
std::max(found->second.snapshot.revision.edit + 1, stored.value->revision);
found->second.snapshot.revision.edit = synchronizedEdit;
found->second.snapshot.revision.saved = synchronizedEdit;
found->second.snapshot.revision.disk = stored.value->revision;
found->second.snapshot.diskContentHash = stored.value->contentHash;
found->second.snapshot.state = DocumentState::Ready;
found->second.snapshot.diagnostics.clear();
return EditorResult<DocumentSnapshot>::applied(found->second.snapshot);
}
EditorResult<void> DocumentService::close(const DocumentId& document) {
if (!open_.erase(document))
return EditorResult<void>::error(EditorStatus::NotFound, RuleId("editor.document.not-open"),
"Document is not open");
std::erase_if(pendingSaves_, [&](const auto& entry) { return entry.second.ticket.document == document; });
return EditorResult<void>::applied();
}
EditorResult<DocumentSnapshot> DocumentService::error(EditorStatus status, const char* rule, std::string message) {
return EditorResult<DocumentSnapshot>::error(status, RuleId(rule), std::move(message));
}
} // namespace eve::editor