mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Integrate appflowy editor (#1040)
This commit is contained in:
@ -28,6 +28,7 @@ tokio = {version = "1", features = ["sync"]}
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
|
||||
bytes = { version = "1.1" }
|
||||
md5 = "0.7.0"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
dashmap = "5"
|
||||
|
419
frontend/rust-lib/flowy-document/src/editor/READ_ME.json
Normal file
419
frontend/rust-lib/flowy-document/src/editor/READ_ME.json
Normal file
@ -0,0 +1,419 @@
|
||||
{
|
||||
"document": {
|
||||
"type": "editor",
|
||||
"children": [
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"heading": "h1"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "🌟 Welcome to AppFlowy!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"heading": "h2"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Here are the basics"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click anywhere and just start typing."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Highlight",
|
||||
"attributes": {
|
||||
"backgroundColor": "0x6000BCF0"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " any text, and use the editing menu to "
|
||||
},
|
||||
{
|
||||
"insert": "style",
|
||||
"attributes": {
|
||||
"italic": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "your",
|
||||
"attributes": {
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "writing",
|
||||
"attributes": {
|
||||
"underline": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "however",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " you "
|
||||
},
|
||||
{
|
||||
"insert": "like.",
|
||||
"attributes": {
|
||||
"strikethrough": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "As soon as you type "
|
||||
},
|
||||
{
|
||||
"insert": "/",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " a menu will pop up. Select different types of content blocks you can add."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Type "
|
||||
},
|
||||
{
|
||||
"insert": "/",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " followed by "
|
||||
},
|
||||
{
|
||||
"insert": "/bullet",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " or "
|
||||
},
|
||||
{
|
||||
"insert": "/c.",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": true
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click "
|
||||
},
|
||||
{
|
||||
"insert": "+ New Page ",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "button at the bottom of your sidebar to add a new page."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "checkbox",
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click "
|
||||
},
|
||||
{
|
||||
"insert": "+",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " next to any page title in the sidebar to quickly add a new subpage."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": []
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"checkbox": null,
|
||||
"heading": "h2"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Markdown"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 1,
|
||||
"heading": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Heading "
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 2
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "bold text",
|
||||
"attributes": {
|
||||
"bold": true,
|
||||
"defaultFormating": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 3
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "italicized text",
|
||||
"attributes": {
|
||||
"italic": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "number-list",
|
||||
"number": 4,
|
||||
"number-list": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Ordered List"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"number": 5,
|
||||
"subtype": "number-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "code",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"number": 6,
|
||||
"subtype": "number-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Strikethrough",
|
||||
"attributes": {
|
||||
"strikethrough": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"retain": 1,
|
||||
"attributes": {
|
||||
"strikethrough": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"checkbox": null
|
||||
},
|
||||
"delta": []
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"checkbox": null,
|
||||
"heading": "h2"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Have a question?"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "quote"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Click "
|
||||
},
|
||||
{
|
||||
"insert": "?",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " at the bottom right for help and support."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"delta": []
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "heading",
|
||||
"heading": "h2"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Like AppFlowy? Follow us:"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "bulleted-list",
|
||||
"quote": null
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "GitHub",
|
||||
"attributes": {
|
||||
"href": "https://github.com/AppFlowy-IO/AppFlowy"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "bulleted-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Twitter: @appflowy"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": "bulleted-list"
|
||||
},
|
||||
"delta": [
|
||||
{
|
||||
"insert": "Newsletter",
|
||||
"attributes": {
|
||||
"href": "https://blog-appflowy.ghost.io/"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "text",
|
||||
"attributes": {
|
||||
"subtype": null,
|
||||
"heading": null
|
||||
},
|
||||
"delta": []
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
use bytes::Bytes;
|
||||
use flowy_error::{FlowyError, FlowyResult};
|
||||
use flowy_revision::{RevisionObjectDeserializer, RevisionObjectSerializer};
|
||||
use flowy_revision::{RevisionCompress, RevisionObjectDeserializer, RevisionObjectSerializer};
|
||||
use flowy_sync::entities::revision::Revision;
|
||||
use lib_ot::core::{
|
||||
Body, Extension, NodeDataBuilder, NodeOperation, NodeTree, NodeTreeContext, Selection, Transaction,
|
||||
};
|
||||
use lib_ot::text_delta::TextOperationBuilder;
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Document {
|
||||
@ -30,6 +30,11 @@ impl Document {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn md5(&self) -> String {
|
||||
// format!("{:x}", md5::compute(bytes))
|
||||
"".to_owned()
|
||||
}
|
||||
|
||||
pub fn get_tree(&self) -> &NodeTree {
|
||||
&self.tree
|
||||
}
|
||||
@ -40,7 +45,7 @@ pub(crate) fn make_tree_context() -> NodeTreeContext {
|
||||
}
|
||||
|
||||
pub fn initial_document_content() -> String {
|
||||
let delta = TextOperationBuilder::new().insert("").build();
|
||||
let delta = DeltaTextOperationBuilder::new().insert("").build();
|
||||
let node_data = NodeDataBuilder::new("text").insert_body(Body::Delta(delta)).build();
|
||||
let editor_node = NodeDataBuilder::new("editor").add_node_data(node_data).build();
|
||||
let node_operation = NodeOperation::Insert {
|
||||
@ -78,7 +83,7 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde {
|
||||
|
||||
fn deserialize_revisions(_object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
|
||||
let mut tree = NodeTree::new(make_tree_context());
|
||||
let transaction = make_transaction_from_revisions(revisions)?;
|
||||
let transaction = make_transaction_from_revisions(&revisions)?;
|
||||
let _ = tree.apply_transaction(transaction)?;
|
||||
let document = Document::new(tree);
|
||||
Result::<Document, FlowyError>::Ok(document)
|
||||
@ -87,12 +92,20 @@ impl RevisionObjectDeserializer for DocumentRevisionSerde {
|
||||
|
||||
impl RevisionObjectSerializer for DocumentRevisionSerde {
|
||||
fn combine_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||
let transaction = make_transaction_from_revisions(revisions)?;
|
||||
let transaction = make_transaction_from_revisions(&revisions)?;
|
||||
Ok(Bytes::from(transaction.to_bytes()?))
|
||||
}
|
||||
}
|
||||
|
||||
fn make_transaction_from_revisions(revisions: Vec<Revision>) -> FlowyResult<Transaction> {
|
||||
pub(crate) struct DocumentRevisionCompress();
|
||||
impl RevisionCompress for DocumentRevisionCompress {
|
||||
fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||
DocumentRevisionSerde::combine_revisions(revisions)
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
pub fn make_transaction_from_revisions(revisions: &[Revision]) -> FlowyResult<Transaction> {
|
||||
let mut transaction = Transaction::new();
|
||||
for revision in revisions {
|
||||
let _ = transaction.compose(Transaction::from_bytes(&revision.bytes)?)?;
|
||||
|
@ -3,11 +3,12 @@ use crate::editor::document::Document;
|
||||
use bytes::Bytes;
|
||||
use flowy_error::FlowyResult;
|
||||
use lib_ot::core::{
|
||||
AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, Path, Selection,
|
||||
Transaction,
|
||||
AttributeHashMap, Body, Changeset, Extension, NodeData, NodeId, NodeOperation, NodeTree, NodeTreeContext, Path,
|
||||
Selection, Transaction,
|
||||
};
|
||||
use lib_ot::text_delta::TextOperations;
|
||||
use serde::de::{self, MapAccess, Visitor};
|
||||
|
||||
use lib_ot::text_delta::DeltaTextOperations;
|
||||
use serde::de::{self, MapAccess, Unexpected, Visitor};
|
||||
use serde::ser::{SerializeMap, SerializeSeq};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use std::fmt;
|
||||
@ -44,14 +45,14 @@ impl<'de> Deserialize<'de> for Document {
|
||||
where
|
||||
M: MapAccess<'de>,
|
||||
{
|
||||
let mut node_tree = None;
|
||||
let mut document_node = None;
|
||||
while let Some(key) = map.next_key()? {
|
||||
match key {
|
||||
"document" => {
|
||||
if node_tree.is_some() {
|
||||
if document_node.is_some() {
|
||||
return Err(de::Error::duplicate_field("document"));
|
||||
}
|
||||
node_tree = Some(map.next_value::<NodeTree>()?)
|
||||
document_node = Some(map.next_value::<DocumentNode>()?)
|
||||
}
|
||||
s => {
|
||||
return Err(de::Error::unknown_field(s, FIELDS));
|
||||
@ -59,8 +60,13 @@ impl<'de> Deserialize<'de> for Document {
|
||||
}
|
||||
}
|
||||
|
||||
match node_tree {
|
||||
Some(tree) => Ok(Document::new(tree)),
|
||||
match document_node {
|
||||
Some(document_node) => {
|
||||
match NodeTree::from_node_data(document_node.into(), NodeTreeContext::default()) {
|
||||
Ok(tree) => Ok(Document::new(tree)),
|
||||
Err(err) => Err(de::Error::invalid_value(Unexpected::Other(&format!("{}", err)), &"")),
|
||||
}
|
||||
}
|
||||
None => Err(de::Error::missing_field("document")),
|
||||
}
|
||||
}
|
||||
@ -69,10 +75,20 @@ impl<'de> Deserialize<'de> for Document {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DocumentContentSerializer<'a>(pub &'a Document);
|
||||
pub fn make_transaction_from_document_content(content: &str) -> FlowyResult<Transaction> {
|
||||
let document_node: DocumentNode = serde_json::from_str::<DocumentContentDeserializer>(content)?.document;
|
||||
let document_operation = DocumentOperation::Insert {
|
||||
path: 0_usize.into(),
|
||||
nodes: vec![document_node],
|
||||
};
|
||||
let mut document_transaction = DocumentTransaction::default();
|
||||
document_transaction.operations.push(document_operation);
|
||||
Ok(document_transaction.into())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DocumentContentSerde {}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct DocumentTransaction {
|
||||
#[serde(default)]
|
||||
operations: Vec<DocumentOperation>,
|
||||
@ -161,8 +177,8 @@ pub enum DocumentOperation {
|
||||
#[serde(rename = "update_text")]
|
||||
UpdateText {
|
||||
path: Path,
|
||||
delta: TextOperations,
|
||||
inverted: TextOperations,
|
||||
delta: DeltaTextOperations,
|
||||
inverted: DeltaTextOperations,
|
||||
},
|
||||
}
|
||||
|
||||
@ -230,20 +246,27 @@ pub struct DocumentNode {
|
||||
#[serde(default)]
|
||||
pub attributes: AttributeHashMap,
|
||||
|
||||
#[serde(skip_serializing_if = "TextOperations::is_empty")]
|
||||
pub delta: TextOperations,
|
||||
#[serde(skip_serializing_if = "DeltaTextOperations::is_empty")]
|
||||
#[serde(default)]
|
||||
pub delta: DeltaTextOperations,
|
||||
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
#[serde(default)]
|
||||
pub children: Vec<DocumentNode>,
|
||||
}
|
||||
|
||||
impl DocumentNode {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<NodeData> for DocumentNode {
|
||||
fn from(node_data: NodeData) -> Self {
|
||||
let delta = if let Body::Delta(operations) = node_data.body {
|
||||
operations
|
||||
} else {
|
||||
TextOperations::default()
|
||||
DeltaTextOperations::default()
|
||||
};
|
||||
DocumentNode {
|
||||
node_type: node_data.node_type,
|
||||
@ -265,6 +288,14 @@ impl std::convert::From<DocumentNode> for NodeData {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct DocumentContentDeserializer {
|
||||
document: DocumentNode,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DocumentContentSerializer<'a>(pub &'a Document);
|
||||
|
||||
impl<'a> Serialize for DocumentContentSerializer<'a> {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
@ -299,6 +330,12 @@ impl<'a> Serialize for DocumentContentSerializer<'a> {
|
||||
mod tests {
|
||||
use crate::editor::document::Document;
|
||||
use crate::editor::document_serde::DocumentTransaction;
|
||||
use crate::editor::initial_read_me;
|
||||
|
||||
#[test]
|
||||
fn load_read_me() {
|
||||
let _ = initial_read_me();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transaction_deserialize_update_text_operation_test() {
|
||||
|
@ -1,5 +1,6 @@
|
||||
use crate::editor::document::{Document, DocumentRevisionSerde};
|
||||
use crate::editor::document_serde::DocumentTransaction;
|
||||
use crate::editor::make_transaction_from_revisions;
|
||||
use crate::editor::queue::{Command, CommandSender, DocumentQueue};
|
||||
use crate::{DocumentEditor, DocumentUser};
|
||||
use bytes::Bytes;
|
||||
@ -17,6 +18,7 @@ pub struct AppFlowyDocumentEditor {
|
||||
#[allow(dead_code)]
|
||||
doc_id: String,
|
||||
command_sender: CommandSender,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
}
|
||||
|
||||
impl AppFlowyDocumentEditor {
|
||||
@ -28,9 +30,13 @@ impl AppFlowyDocumentEditor {
|
||||
) -> FlowyResult<Arc<Self>> {
|
||||
let document = rev_manager.load::<DocumentRevisionSerde>(Some(cloud_service)).await?;
|
||||
let rev_manager = Arc::new(rev_manager);
|
||||
let command_sender = spawn_edit_queue(user, rev_manager, document);
|
||||
let command_sender = spawn_edit_queue(user, rev_manager.clone(), document);
|
||||
let doc_id = doc_id.to_string();
|
||||
let editor = Arc::new(Self { doc_id, command_sender });
|
||||
let editor = Arc::new(Self {
|
||||
doc_id,
|
||||
command_sender,
|
||||
rev_manager,
|
||||
});
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
@ -53,6 +59,13 @@ impl AppFlowyDocumentEditor {
|
||||
let content = rx.await.map_err(internal_error)??;
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
pub async fn duplicate_document(&self) -> FlowyResult<String> {
|
||||
let revisions = self.rev_manager.load_revisions().await?;
|
||||
let transaction = make_transaction_from_revisions(&revisions)?;
|
||||
let json = transaction.to_json()?;
|
||||
Ok(json)
|
||||
}
|
||||
}
|
||||
|
||||
fn spawn_edit_queue(
|
||||
@ -67,11 +80,24 @@ fn spawn_edit_queue(
|
||||
}
|
||||
|
||||
impl DocumentEditor for Arc<AppFlowyDocumentEditor> {
|
||||
fn close(&self) {}
|
||||
|
||||
fn export(&self) -> FutureResult<String, FlowyError> {
|
||||
let this = self.clone();
|
||||
FutureResult::new(async move { this.get_content(false).await })
|
||||
}
|
||||
|
||||
fn duplicate(&self) -> FutureResult<String, FlowyError> {
|
||||
let this = self.clone();
|
||||
FutureResult::new(async move { this.duplicate_document().await })
|
||||
}
|
||||
|
||||
fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> {
|
||||
FutureResult::new(async move { Ok(()) })
|
||||
}
|
||||
|
||||
fn receive_ws_state(&self, _state: &WSConnectState) {}
|
||||
|
||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
|
||||
let this = self.clone();
|
||||
FutureResult::new(async move {
|
||||
@ -81,14 +107,6 @@ impl DocumentEditor for Arc<AppFlowyDocumentEditor> {
|
||||
})
|
||||
}
|
||||
|
||||
fn close(&self) {}
|
||||
|
||||
fn receive_ws_data(&self, _data: ServerRevisionWSData) -> FutureResult<(), FlowyError> {
|
||||
FutureResult::new(async move { Ok(()) })
|
||||
}
|
||||
|
||||
fn receive_ws_state(&self, _state: &WSConnectState) {}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
|
@ -0,0 +1,419 @@
|
||||
use crate::editor::{DocumentNode, DocumentOperation};
|
||||
use flowy_error::FlowyResult;
|
||||
|
||||
use lib_ot::core::{AttributeHashMap, DeltaOperation, Insert, Transaction};
|
||||
use lib_ot::text_delta::{DeltaTextOperation, DeltaTextOperations};
|
||||
|
||||
pub struct DeltaRevisionMigration();
|
||||
|
||||
impl DeltaRevisionMigration {
|
||||
pub fn run(delta: DeltaTextOperations) -> FlowyResult<Transaction> {
|
||||
let migrate_background_attribute = |insert: &mut Insert<AttributeHashMap>| {
|
||||
if let Some(Some(color)) = insert.attributes.get("background").map(|value| value.str_value()) {
|
||||
insert.attributes.remove_key("background");
|
||||
insert.attributes.insert("backgroundColor", color);
|
||||
}
|
||||
};
|
||||
let migrate_strike_attribute = |insert: &mut Insert<AttributeHashMap>| {
|
||||
if let Some(Some(_)) = insert.attributes.get("strike").map(|value| value.str_value()) {
|
||||
insert.attributes.remove_key("strike");
|
||||
insert.attributes.insert("strikethrough", true);
|
||||
}
|
||||
};
|
||||
|
||||
let migrate_link_attribute = |insert: &mut Insert<AttributeHashMap>| {
|
||||
if let Some(Some(link)) = insert.attributes.get("link").map(|value| value.str_value()) {
|
||||
insert.attributes.remove_key("link");
|
||||
insert.attributes.insert("href", link);
|
||||
}
|
||||
};
|
||||
|
||||
let migrate_list_attribute =
|
||||
|attribute_node: &mut DocumentNode, value: &str, number_list_number: &mut usize| {
|
||||
if value == "unchecked" {
|
||||
*number_list_number = 0;
|
||||
attribute_node.attributes.insert("subtype", "checkbox");
|
||||
attribute_node.attributes.insert("checkbox", false);
|
||||
}
|
||||
if value == "checked" {
|
||||
*number_list_number = 0;
|
||||
attribute_node.attributes.insert("subtype", "checkbox");
|
||||
attribute_node.attributes.insert("checkbox", true);
|
||||
}
|
||||
|
||||
if value == "bullet" {
|
||||
*number_list_number = 0;
|
||||
attribute_node.attributes.insert("subtype", "bulleted-list");
|
||||
}
|
||||
|
||||
if value == "ordered" {
|
||||
*number_list_number += 1;
|
||||
attribute_node.attributes.insert("subtype", "number-list");
|
||||
attribute_node.attributes.insert("number", *number_list_number);
|
||||
}
|
||||
};
|
||||
|
||||
let generate_new_op_with_double_new_lines = |insert: &mut Insert<AttributeHashMap>| {
|
||||
let pattern = "\n\n";
|
||||
let mut new_ops = vec![];
|
||||
if insert.s.as_str().contains(pattern) {
|
||||
let insert_str = insert.s.clone();
|
||||
let insert_strings = insert_str.split(pattern).map(|s| s.to_owned());
|
||||
for (index, new_s) in insert_strings.enumerate() {
|
||||
if index == 0 {
|
||||
insert.s = new_s.into();
|
||||
} else {
|
||||
new_ops.push(DeltaOperation::Insert(Insert {
|
||||
s: new_s.into(),
|
||||
attributes: AttributeHashMap::default(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
new_ops
|
||||
};
|
||||
|
||||
let create_text_node = |ops: Vec<DeltaTextOperation>| {
|
||||
let mut document_node = DocumentNode::new();
|
||||
document_node.node_type = "text".to_owned();
|
||||
ops.into_iter().for_each(|op| document_node.delta.add(op));
|
||||
document_node
|
||||
};
|
||||
|
||||
let transform_op = |mut insert: Insert<AttributeHashMap>| {
|
||||
// Rename the attribute name from background to backgroundColor
|
||||
migrate_background_attribute(&mut insert);
|
||||
migrate_strike_attribute(&mut insert);
|
||||
migrate_link_attribute(&mut insert);
|
||||
|
||||
let new_ops = generate_new_op_with_double_new_lines(&mut insert);
|
||||
(DeltaOperation::Insert(insert), new_ops)
|
||||
};
|
||||
let mut index: usize = 0;
|
||||
let mut number_list_number = 0;
|
||||
let mut editor_node = DocumentNode::new();
|
||||
editor_node.node_type = "editor".to_owned();
|
||||
|
||||
let mut transaction = Transaction::new();
|
||||
transaction.push_operation(DocumentOperation::Insert {
|
||||
path: 0.into(),
|
||||
nodes: vec![editor_node],
|
||||
});
|
||||
|
||||
let mut iter = delta.ops.into_iter().enumerate();
|
||||
while let Some((_, op)) = iter.next() {
|
||||
let mut document_node = create_text_node(vec![]);
|
||||
let mut split_document_nodes = vec![];
|
||||
match op {
|
||||
DeltaOperation::Delete(_) => tracing::warn!("Should not contain delete operation"),
|
||||
DeltaOperation::Retain(_) => tracing::warn!("Should not contain retain operation"),
|
||||
DeltaOperation::Insert(insert) => {
|
||||
if insert.s.as_str() != "\n" {
|
||||
let (op, new_ops) = transform_op(insert);
|
||||
document_node.delta.add(op);
|
||||
if !new_ops.is_empty() {
|
||||
split_document_nodes.push(create_text_node(new_ops));
|
||||
}
|
||||
}
|
||||
|
||||
while let Some((_, DeltaOperation::Insert(insert))) = iter.next() {
|
||||
if insert.s.as_str() != "\n" {
|
||||
let (op, new_ops) = transform_op(insert);
|
||||
document_node.delta.add(op);
|
||||
|
||||
if !new_ops.is_empty() {
|
||||
split_document_nodes.push(create_text_node(new_ops));
|
||||
}
|
||||
} else {
|
||||
let attribute_node = match split_document_nodes.last_mut() {
|
||||
None => &mut document_node,
|
||||
Some(split_document_node) => split_document_node,
|
||||
};
|
||||
|
||||
if let Some(value) = insert.attributes.get("header") {
|
||||
attribute_node.attributes.insert("subtype", "heading");
|
||||
if let Some(v) = value.int_value() {
|
||||
number_list_number = 0;
|
||||
attribute_node.attributes.insert("heading", format!("h{}", v));
|
||||
}
|
||||
}
|
||||
|
||||
if insert.attributes.get("blockquote").is_some() {
|
||||
attribute_node.attributes.insert("subtype", "quote");
|
||||
}
|
||||
|
||||
if let Some(value) = insert.attributes.get("list") {
|
||||
if let Some(s) = value.str_value() {
|
||||
migrate_list_attribute(attribute_node, &s, &mut number_list_number);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut operations = vec![document_node];
|
||||
operations.extend(split_document_nodes);
|
||||
operations.into_iter().for_each(|node| {
|
||||
// println!("{}", serde_json::to_string(&node).unwrap());
|
||||
let operation = DocumentOperation::Insert {
|
||||
path: vec![0, index].into(),
|
||||
nodes: vec![node],
|
||||
};
|
||||
transaction.push_operation(operation);
|
||||
index += 1;
|
||||
});
|
||||
}
|
||||
Ok(transaction)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::editor::migration::delta_migration::DeltaRevisionMigration;
|
||||
use crate::editor::Document;
|
||||
use lib_ot::text_delta::DeltaTextOperations;
|
||||
|
||||
#[test]
|
||||
fn transform_delta_to_transaction_test() {
|
||||
let delta = DeltaTextOperations::from_json(DELTA_STR).unwrap();
|
||||
let transaction = DeltaRevisionMigration::run(delta).unwrap();
|
||||
let document = Document::from_transaction(transaction).unwrap();
|
||||
let s = document.get_content(true).unwrap();
|
||||
assert!(!s.is_empty());
|
||||
}
|
||||
|
||||
const DELTA_STR: &str = r#"[
|
||||
{
|
||||
"insert": "\n👋 Welcome to AppFlowy!"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"header": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "\nHere are the basics"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"header": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "Click anywhere and just start typing"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"list": "unchecked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "Highlight",
|
||||
"attributes": {
|
||||
"background": "$fff2cd"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " any text, and use the menu at the bottom to "
|
||||
},
|
||||
{
|
||||
"insert": "style",
|
||||
"attributes": {
|
||||
"italic": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "your",
|
||||
"attributes": {
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "writing",
|
||||
"attributes": {
|
||||
"underline": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "however",
|
||||
"attributes": {
|
||||
"code": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "you",
|
||||
"attributes": {
|
||||
"strike": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " "
|
||||
},
|
||||
{
|
||||
"insert": "like",
|
||||
"attributes": {
|
||||
"background": "$e8e0ff"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"list": "unchecked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "Click "
|
||||
},
|
||||
{
|
||||
"insert": "+ New Page",
|
||||
"attributes": {
|
||||
"background": "$defff1",
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " button at the bottom of your sidebar to add a new page"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"list": "unchecked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "Click the "
|
||||
},
|
||||
{
|
||||
"insert": "'",
|
||||
"attributes": {
|
||||
"background": "$defff1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "+",
|
||||
"attributes": {
|
||||
"background": "$defff1",
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "'",
|
||||
"attributes": {
|
||||
"background": "$defff1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " next to any page title in the sidebar to quickly add a new subpage"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"list": "unchecked"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "\nHave a question? "
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"header": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "Click the "
|
||||
},
|
||||
{
|
||||
"insert": "'?'",
|
||||
"attributes": {
|
||||
"background": "$defff1",
|
||||
"bold": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": " at the bottom right for help and support.\n\nLike AppFlowy? Follow us:"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"header": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "GitHub: https://github.com/AppFlowy-IO/appflowy"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"blockquote": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "Twitter: https://twitter.com/appflowy"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"blockquote": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "Newsletter: https://www.appflowy.io/blog"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"blockquote": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "item 1"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"list": "ordered"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "item 2"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"list": "ordered"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "item3"
|
||||
},
|
||||
{
|
||||
"insert": "\n",
|
||||
"attributes": {
|
||||
"list": "ordered"
|
||||
}
|
||||
},
|
||||
{
|
||||
"insert": "appflowy",
|
||||
"attributes": {
|
||||
"link": "https://www.appflowy.io/"
|
||||
}
|
||||
}
|
||||
]"#;
|
||||
}
|
@ -0,0 +1,3 @@
|
||||
mod delta_migration;
|
||||
|
||||
pub use delta_migration::*;
|
@ -2,7 +2,17 @@
|
||||
mod document;
|
||||
mod document_serde;
|
||||
mod editor;
|
||||
mod migration;
|
||||
mod queue;
|
||||
|
||||
pub use document::*;
|
||||
pub use document_serde::*;
|
||||
pub use editor::*;
|
||||
pub use migration::*;
|
||||
|
||||
#[inline]
|
||||
pub fn initial_read_me() -> String {
|
||||
let document_content = include_str!("READ_ME.json");
|
||||
let transaction = make_transaction_from_document_content(document_content).unwrap();
|
||||
transaction.to_json().unwrap()
|
||||
}
|
||||
|
@ -1,13 +1,17 @@
|
||||
use crate::editor::document::Document;
|
||||
use crate::DocumentUser;
|
||||
use async_stream::stream;
|
||||
use bytes::Bytes;
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_revision::RevisionManager;
|
||||
use flowy_sync::entities::revision::{RevId, Revision};
|
||||
use futures::stream::StreamExt;
|
||||
use lib_ot::core::Transaction;
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
use tokio::sync::{oneshot, RwLock};
|
||||
|
||||
pub struct DocumentQueue {
|
||||
#[allow(dead_code)]
|
||||
user: Arc<dyn DocumentUser>,
|
||||
@ -56,7 +60,10 @@ impl DocumentQueue {
|
||||
async fn handle_command(&self, command: Command) -> Result<(), FlowyError> {
|
||||
match command {
|
||||
Command::ComposeTransaction { transaction, ret } => {
|
||||
self.document.write().await.apply_transaction(transaction)?;
|
||||
self.document.write().await.apply_transaction(transaction.clone())?;
|
||||
let _ = self
|
||||
.save_local_operations(transaction, self.document.read().await.md5())
|
||||
.await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
Command::GetDocumentContent { pretty, ret } => {
|
||||
@ -66,6 +73,16 @@ impl DocumentQueue {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, transaction, md5), err)]
|
||||
async fn save_local_operations(&self, transaction: Transaction, md5: String) -> Result<RevId, FlowyError> {
|
||||
let bytes = Bytes::from(transaction.to_bytes()?);
|
||||
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
|
||||
let user_id = self.user.user_id()?;
|
||||
let revision = Revision::new(&self.rev_manager.object_id, base_rev_id, rev_id, bytes, &user_id, md5);
|
||||
let _ = self.rev_manager.add_local_revision(&revision).await?;
|
||||
Ok(rev_id.into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type CommandSender = Sender<Command>;
|
||||
|
@ -74,12 +74,41 @@ pub struct ExportPayloadPB {
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub export_type: ExportType,
|
||||
|
||||
#[pb(index = 3)]
|
||||
pub document_version: DocumentVersionPB,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, ProtoBuf_Enum, Clone)]
|
||||
pub enum DocumentVersionPB {
|
||||
/// this version's content of the document is build from `Delta`. It uses
|
||||
/// `DeltaDocumentEditor`.
|
||||
V0 = 0,
|
||||
/// this version's content of the document is build from `NodeTree`. It uses
|
||||
/// `AppFlowyDocumentEditor`
|
||||
V1 = 1,
|
||||
}
|
||||
|
||||
impl std::default::Default for DocumentVersionPB {
|
||||
fn default() -> Self {
|
||||
Self::V0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct OpenDocumentContextPB {
|
||||
#[pb(index = 1)]
|
||||
pub document_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub document_version: DocumentVersionPB,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ExportParams {
|
||||
pub view_id: String,
|
||||
pub export_type: ExportType,
|
||||
pub document_version: DocumentVersionPB,
|
||||
}
|
||||
|
||||
impl TryInto<ExportParams> for ExportPayloadPB {
|
||||
@ -88,6 +117,7 @@ impl TryInto<ExportParams> for ExportPayloadPB {
|
||||
Ok(ExportParams {
|
||||
view_id: self.view_id,
|
||||
export_type: self.export_type,
|
||||
document_version: self.document_version,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +1,23 @@
|
||||
use crate::entities::{DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB};
|
||||
use crate::entities::{
|
||||
DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB, OpenDocumentContextPB,
|
||||
};
|
||||
use crate::DocumentManager;
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_sync::entities::document::DocumentIdPB;
|
||||
|
||||
use lib_dispatch::prelude::{data_result, AppData, Data, DataResult};
|
||||
use std::convert::TryInto;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub(crate) async fn get_document_handler(
|
||||
data: Data<DocumentIdPB>,
|
||||
data: Data<OpenDocumentContextPB>,
|
||||
manager: AppData<Arc<DocumentManager>>,
|
||||
) -> DataResult<DocumentSnapshotPB, FlowyError> {
|
||||
let document_id: DocumentIdPB = data.into_inner();
|
||||
let editor = manager.open_document_editor(&document_id).await?;
|
||||
let operations_str = editor.export().await?;
|
||||
let context: OpenDocumentContextPB = data.into_inner();
|
||||
let editor = manager.open_document_editor(&context.document_id).await?;
|
||||
let document_data = editor.export().await?;
|
||||
data_result(DocumentSnapshotPB {
|
||||
doc_id: document_id.into(),
|
||||
snapshot: operations_str,
|
||||
doc_id: context.document_id,
|
||||
snapshot: document_data,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,7 @@ pub fn create(document_manager: Arc<DocumentManager>) -> Module {
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
#[event_err = "FlowyError"]
|
||||
pub enum DocumentEvent {
|
||||
#[event(input = "DocumentIdPB", output = "DocumentSnapshotPB")]
|
||||
#[event(input = "OpenDocumentContextPB", output = "DocumentSnapshotPB")]
|
||||
GetDocument = 0,
|
||||
|
||||
#[event(input = "EditPayloadPB")]
|
||||
|
@ -1,4 +1,4 @@
|
||||
mod entities;
|
||||
pub mod entities;
|
||||
mod event_handler;
|
||||
pub mod event_map;
|
||||
pub mod manager;
|
||||
@ -6,6 +6,7 @@ pub mod manager;
|
||||
pub mod editor;
|
||||
pub mod old_editor;
|
||||
pub mod protobuf;
|
||||
mod services;
|
||||
|
||||
pub use manager::*;
|
||||
pub mod errors {
|
||||
|
@ -1,23 +1,23 @@
|
||||
use crate::editor::{initial_document_content, AppFlowyDocumentEditor};
|
||||
use crate::entities::EditParams;
|
||||
use crate::old_editor::editor::{DeltaDocumentEditor, DocumentRevisionCompress};
|
||||
use crate::editor::{initial_document_content, AppFlowyDocumentEditor, DocumentRevisionCompress};
|
||||
use crate::entities::{DocumentVersionPB, EditParams};
|
||||
use crate::old_editor::editor::{DeltaDocumentEditor, DeltaDocumentRevisionCompress};
|
||||
use crate::services::DocumentPersistence;
|
||||
use crate::{errors::FlowyError, DocumentCloudService};
|
||||
use bytes::Bytes;
|
||||
use dashmap::DashMap;
|
||||
use flowy_database::ConnectionPool;
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_revision::disk::SQLiteDocumentRevisionPersistence;
|
||||
use flowy_revision::disk::{SQLiteDeltaDocumentRevisionPersistence, SQLiteDocumentRevisionPersistence};
|
||||
use flowy_revision::{
|
||||
RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence,
|
||||
};
|
||||
use flowy_sync::client_document::initial_old_document_content;
|
||||
use flowy_sync::client_document::initial_delta_document_content;
|
||||
use flowy_sync::entities::{
|
||||
document::{DocumentIdPB, DocumentOperationsPB},
|
||||
document::DocumentIdPB,
|
||||
revision::{md5, RepeatedRevision, Revision},
|
||||
ws_data::ServerRevisionWSData,
|
||||
};
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
use lib_ws::WSConnectState;
|
||||
use std::any::Any;
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
@ -26,17 +26,31 @@ pub trait DocumentUser: Send + Sync {
|
||||
fn user_dir(&self) -> Result<String, FlowyError>;
|
||||
fn user_id(&self) -> Result<String, FlowyError>;
|
||||
fn token(&self) -> Result<String, FlowyError>;
|
||||
}
|
||||
|
||||
pub trait DocumentDatabase: Send + Sync {
|
||||
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
|
||||
}
|
||||
|
||||
pub trait DocumentEditor: Send + Sync {
|
||||
fn export(&self) -> FutureResult<String, FlowyError>;
|
||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>;
|
||||
/// Called when the document get closed
|
||||
fn close(&self);
|
||||
|
||||
/// Exports the document content. The content is encoded in the corresponding
|
||||
/// editor data format.
|
||||
fn export(&self) -> FutureResult<String, FlowyError>;
|
||||
|
||||
/// Duplicate the document inner data into String
|
||||
fn duplicate(&self) -> FutureResult<String, FlowyError>;
|
||||
|
||||
fn receive_ws_data(&self, data: ServerRevisionWSData) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn receive_ws_state(&self, state: &WSConnectState);
|
||||
|
||||
/// Receives the local operations made by the user input. The operations are encoded
|
||||
/// in binary format.
|
||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError>;
|
||||
|
||||
/// Returns the `Any` reference that can be used to downcast back to the original,
|
||||
/// concrete type.
|
||||
///
|
||||
@ -50,7 +64,15 @@ pub trait DocumentEditor: Send + Sync {
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DocumentConfig {
|
||||
pub use_new_editor: bool,
|
||||
pub version: DocumentVersionPB,
|
||||
}
|
||||
|
||||
impl std::default::Default for DocumentConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
version: DocumentVersionPB::V1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DocumentManager {
|
||||
@ -58,6 +80,8 @@ pub struct DocumentManager {
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
editor_map: Arc<DocumentEditorMap>,
|
||||
user: Arc<dyn DocumentUser>,
|
||||
persistence: Arc<DocumentPersistence>,
|
||||
#[allow(dead_code)]
|
||||
config: DocumentConfig,
|
||||
}
|
||||
|
||||
@ -65,6 +89,7 @@ impl DocumentManager {
|
||||
pub fn new(
|
||||
cloud_service: Arc<dyn DocumentCloudService>,
|
||||
document_user: Arc<dyn DocumentUser>,
|
||||
database: Arc<dyn DocumentDatabase>,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
config: DocumentConfig,
|
||||
) -> Self {
|
||||
@ -73,24 +98,31 @@ impl DocumentManager {
|
||||
rev_web_socket,
|
||||
editor_map: Arc::new(DocumentEditorMap::new()),
|
||||
user: document_user,
|
||||
persistence: Arc::new(DocumentPersistence::new(database)),
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&self) -> FlowyResult<()> {
|
||||
/// Called immediately after the application launched with the user sign in/sign up.
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
pub async fn initialize(&self, user_id: &str) -> FlowyResult<()> {
|
||||
let _ = self.persistence.initialize(user_id)?;
|
||||
listen_ws_state_changed(self.rev_web_socket.clone(), self.editor_map.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
||||
pub async fn initialize_with_new_user(&self, _user_id: &str, _token: &str) -> FlowyResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, fields(document_id), err)]
|
||||
pub async fn open_document_editor<T: AsRef<str>>(
|
||||
&self,
|
||||
editor_id: T,
|
||||
document_id: T,
|
||||
) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
|
||||
let editor_id = editor_id.as_ref();
|
||||
tracing::Span::current().record("editor_id", &editor_id);
|
||||
self.init_document_editor(editor_id).await
|
||||
let document_id = document_id.as_ref();
|
||||
tracing::Span::current().record("document_id", &document_id);
|
||||
self.init_document_editor(document_id).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
||||
@ -101,22 +133,6 @@ impl DocumentManager {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(self, payload), err)]
|
||||
pub async fn receive_local_operations(
|
||||
&self,
|
||||
payload: DocumentOperationsPB,
|
||||
) -> Result<DocumentOperationsPB, FlowyError> {
|
||||
let editor = self.get_document_editor(&payload.doc_id).await?;
|
||||
let _ = editor
|
||||
.compose_local_operations(Bytes::from(payload.operations_str))
|
||||
.await?;
|
||||
let operations_str = editor.export().await?;
|
||||
Ok(DocumentOperationsPB {
|
||||
doc_id: payload.doc_id.clone(),
|
||||
operations_str,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn apply_edit(&self, params: EditParams) -> FlowyResult<()> {
|
||||
let editor = self.get_document_editor(¶ms.doc_id).await?;
|
||||
let _ = editor.compose_local_operations(Bytes::from(params.operations)).await?;
|
||||
@ -125,9 +141,9 @@ impl DocumentManager {
|
||||
|
||||
pub async fn create_document<T: AsRef<str>>(&self, doc_id: T, revisions: RepeatedRevision) -> FlowyResult<()> {
|
||||
let doc_id = doc_id.as_ref().to_owned();
|
||||
let db_pool = self.user.db_pool()?;
|
||||
let db_pool = self.persistence.database.db_pool()?;
|
||||
// Maybe we could save the document to disk without creating the RevisionManager
|
||||
let rev_manager = self.make_document_rev_manager(&doc_id, db_pool)?;
|
||||
let rev_manager = self.make_rev_manager(&doc_id, db_pool)?;
|
||||
let _ = rev_manager.reset_object(revisions).await?;
|
||||
Ok(())
|
||||
}
|
||||
@ -149,10 +165,9 @@ impl DocumentManager {
|
||||
}
|
||||
|
||||
pub fn initial_document_content(&self) -> String {
|
||||
if self.config.use_new_editor {
|
||||
initial_document_content()
|
||||
} else {
|
||||
initial_old_document_content()
|
||||
match self.config.version {
|
||||
DocumentVersionPB::V0 => initial_delta_document_content(),
|
||||
DocumentVersionPB::V1 => initial_document_content(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -168,7 +183,11 @@ impl DocumentManager {
|
||||
///
|
||||
async fn get_document_editor(&self, doc_id: &str) -> FlowyResult<Arc<dyn DocumentEditor>> {
|
||||
match self.editor_map.get(doc_id) {
|
||||
None => self.init_document_editor(doc_id).await,
|
||||
None => {
|
||||
//
|
||||
tracing::warn!("Should call init_document_editor first");
|
||||
self.init_document_editor(doc_id).await
|
||||
}
|
||||
Some(editor) => Ok(editor),
|
||||
}
|
||||
}
|
||||
@ -184,25 +203,39 @@ impl DocumentManager {
|
||||
///
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
pub async fn init_document_editor(&self, doc_id: &str) -> Result<Arc<dyn DocumentEditor>, FlowyError> {
|
||||
let pool = self.user.db_pool()?;
|
||||
let pool = self.persistence.database.db_pool()?;
|
||||
let user = self.user.clone();
|
||||
let token = self.user.token()?;
|
||||
let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?;
|
||||
let cloud_service = Arc::new(DocumentRevisionCloudService {
|
||||
token,
|
||||
server: self.cloud_service.clone(),
|
||||
});
|
||||
|
||||
let editor: Arc<dyn DocumentEditor> = if self.config.use_new_editor {
|
||||
let editor = AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?;
|
||||
Arc::new(editor)
|
||||
} else {
|
||||
let editor =
|
||||
DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?;
|
||||
Arc::new(editor)
|
||||
};
|
||||
self.editor_map.insert(doc_id, editor.clone());
|
||||
Ok(editor)
|
||||
match self.config.version {
|
||||
DocumentVersionPB::V0 => {
|
||||
let rev_manager = self.make_delta_document_rev_manager(doc_id, pool.clone())?;
|
||||
let editor: Arc<dyn DocumentEditor> = Arc::new(
|
||||
DeltaDocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service)
|
||||
.await?,
|
||||
);
|
||||
self.editor_map.insert(doc_id, editor.clone());
|
||||
Ok(editor)
|
||||
}
|
||||
DocumentVersionPB::V1 => {
|
||||
let rev_manager = self.make_document_rev_manager(doc_id, pool.clone())?;
|
||||
let editor: Arc<dyn DocumentEditor> =
|
||||
Arc::new(AppFlowyDocumentEditor::new(doc_id, user, rev_manager, cloud_service).await?);
|
||||
self.editor_map.insert(doc_id, editor.clone());
|
||||
Ok(editor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn make_rev_manager(&self, doc_id: &str, pool: Arc<ConnectionPool>) -> Result<RevisionManager, FlowyError> {
|
||||
match self.config.version {
|
||||
DocumentVersionPB::V0 => self.make_delta_document_rev_manager(doc_id, pool),
|
||||
DocumentVersionPB::V1 => self.make_document_rev_manager(doc_id, pool),
|
||||
}
|
||||
}
|
||||
|
||||
fn make_document_rev_manager(
|
||||
@ -215,13 +248,31 @@ impl DocumentManager {
|
||||
let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
|
||||
// let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone());
|
||||
let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool);
|
||||
let rev_compactor = DocumentRevisionCompress();
|
||||
|
||||
Ok(RevisionManager::new(
|
||||
&user_id,
|
||||
doc_id,
|
||||
rev_persistence,
|
||||
rev_compactor,
|
||||
DocumentRevisionCompress(),
|
||||
// history_persistence,
|
||||
snapshot_persistence,
|
||||
))
|
||||
}
|
||||
|
||||
fn make_delta_document_rev_manager(
|
||||
&self,
|
||||
doc_id: &str,
|
||||
pool: Arc<ConnectionPool>,
|
||||
) -> Result<RevisionManager, FlowyError> {
|
||||
let user_id = self.user.user_id()?;
|
||||
let disk_cache = SQLiteDeltaDocumentRevisionPersistence::new(&user_id, pool.clone());
|
||||
let rev_persistence = RevisionPersistence::new(&user_id, doc_id, disk_cache);
|
||||
// let history_persistence = SQLiteRevisionHistoryPersistence::new(doc_id, pool.clone());
|
||||
let snapshot_persistence = SQLiteRevisionSnapshotPersistence::new(doc_id, pool);
|
||||
Ok(RevisionManager::new(
|
||||
&user_id,
|
||||
doc_id,
|
||||
rev_persistence,
|
||||
DeltaDocumentRevisionCompress(),
|
||||
// history_persistence,
|
||||
snapshot_persistence,
|
||||
))
|
||||
|
@ -18,7 +18,7 @@ use lib_infra::future::FutureResult;
|
||||
use lib_ot::core::{AttributeEntry, AttributeHashMap};
|
||||
use lib_ot::{
|
||||
core::{DeltaOperation, Interval},
|
||||
text_delta::TextOperations,
|
||||
text_delta::DeltaTextOperations,
|
||||
};
|
||||
use lib_ws::WSConnectState;
|
||||
use std::any::Any;
|
||||
@ -46,7 +46,7 @@ impl DeltaDocumentEditor {
|
||||
let document = rev_manager
|
||||
.load::<DeltaDocumentRevisionSerde>(Some(cloud_service))
|
||||
.await?;
|
||||
let operations = TextOperations::from_bytes(&document.content)?;
|
||||
let operations = DeltaTextOperations::from_bytes(&document.content)?;
|
||||
let rev_manager = Arc::new(rev_manager);
|
||||
let doc_id = doc_id.to_string();
|
||||
let user_id = user.user_id()?;
|
||||
@ -147,6 +147,11 @@ impl DeltaDocumentEditor {
|
||||
}
|
||||
|
||||
impl DocumentEditor for Arc<DeltaDocumentEditor> {
|
||||
fn close(&self) {
|
||||
#[cfg(feature = "sync")]
|
||||
self.ws_manager.stop();
|
||||
}
|
||||
|
||||
fn export(&self) -> FutureResult<String, FlowyError> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
|
||||
let msg = EditorCommand::GetOperationsString { ret };
|
||||
@ -158,22 +163,8 @@ impl DocumentEditor for Arc<DeltaDocumentEditor> {
|
||||
})
|
||||
}
|
||||
|
||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
|
||||
let edit_cmd_tx = self.edit_cmd_tx.clone();
|
||||
FutureResult::new(async move {
|
||||
let operations = TextOperations::from_bytes(&data)?;
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||
let msg = EditorCommand::ComposeLocalOperations { operations, ret };
|
||||
|
||||
let _ = edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn close(&self) {
|
||||
#[cfg(feature = "sync")]
|
||||
self.ws_manager.stop();
|
||||
fn duplicate(&self) -> FutureResult<String, FlowyError> {
|
||||
self.export()
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
@ -193,6 +184,19 @@ impl DocumentEditor for Arc<DeltaDocumentEditor> {
|
||||
self.ws_manager.connect_state_changed(state.clone());
|
||||
}
|
||||
|
||||
fn compose_local_operations(&self, data: Bytes) -> FutureResult<(), FlowyError> {
|
||||
let edit_cmd_tx = self.edit_cmd_tx.clone();
|
||||
FutureResult::new(async move {
|
||||
let operations = DeltaTextOperations::from_bytes(&data)?;
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||
let msg = EditorCommand::ComposeLocalOperations { operations, ret };
|
||||
|
||||
let _ = edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
@ -207,7 +211,7 @@ impl std::ops::Drop for DeltaDocumentEditor {
|
||||
fn spawn_edit_queue(
|
||||
user: Arc<dyn DocumentUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
delta: TextOperations,
|
||||
delta: DeltaTextOperations,
|
||||
) -> EditorCommandSender {
|
||||
let (sender, receiver) = mpsc::channel(1000);
|
||||
let edit_queue = EditDocumentQueue::new(user, rev_manager, delta, receiver);
|
||||
@ -226,8 +230,8 @@ fn spawn_edit_queue(
|
||||
|
||||
#[cfg(feature = "flowy_unit_test")]
|
||||
impl DeltaDocumentEditor {
|
||||
pub async fn document_operations(&self) -> FlowyResult<TextOperations> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<TextOperations>>();
|
||||
pub async fn document_operations(&self) -> FlowyResult<DeltaTextOperations> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<DeltaTextOperations>>();
|
||||
let msg = EditorCommand::GetOperations { ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let delta = rx.await.map_err(internal_error)??;
|
||||
@ -264,8 +268,8 @@ impl RevisionObjectSerializer for DeltaDocumentRevisionSerde {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DocumentRevisionCompress();
|
||||
impl RevisionCompress for DocumentRevisionCompress {
|
||||
pub(crate) struct DeltaDocumentRevisionCompress();
|
||||
impl RevisionCompress for DeltaDocumentRevisionCompress {
|
||||
fn combine_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||
DeltaDocumentRevisionSerde::combine_revisions(revisions)
|
||||
}
|
||||
@ -273,7 +277,7 @@ impl RevisionCompress for DocumentRevisionCompress {
|
||||
|
||||
// quill-editor requires the delta should end with '\n' and only contains the
|
||||
// insert operation. The function, correct_delta maybe be removed in the future.
|
||||
fn correct_delta(delta: &mut TextOperations) {
|
||||
fn correct_delta(delta: &mut DeltaTextOperations) {
|
||||
if let Some(op) = delta.ops.last() {
|
||||
let op_data = op.get_data();
|
||||
if !op_data.ends_with('\n') {
|
||||
|
@ -1,4 +1,4 @@
|
||||
use crate::old_editor::web_socket::DocumentResolveOperations;
|
||||
use crate::old_editor::web_socket::DeltaDocumentResolveOperations;
|
||||
use crate::DocumentUser;
|
||||
use async_stream::stream;
|
||||
use flowy_error::FlowyError;
|
||||
@ -12,7 +12,7 @@ use futures::stream::StreamExt;
|
||||
use lib_ot::core::AttributeEntry;
|
||||
use lib_ot::{
|
||||
core::{Interval, OperationTransform},
|
||||
text_delta::TextOperations,
|
||||
text_delta::DeltaTextOperations,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::mpsc::{Receiver, Sender};
|
||||
@ -31,7 +31,7 @@ impl EditDocumentQueue {
|
||||
pub(crate) fn new(
|
||||
user: Arc<dyn DocumentUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
operations: TextOperations,
|
||||
operations: DeltaTextOperations,
|
||||
receiver: EditorCommandReceiver,
|
||||
) -> Self {
|
||||
let document = Arc::new(RwLock::new(ClientDocument::from_operations(operations)));
|
||||
@ -91,8 +91,8 @@ impl EditDocumentQueue {
|
||||
EditorCommand::TransformOperations { operations, ret } => {
|
||||
let f = || async {
|
||||
let read_guard = self.document.read().await;
|
||||
let mut server_operations: Option<DocumentResolveOperations> = None;
|
||||
let client_operations: TextOperations;
|
||||
let mut server_operations: Option<DeltaDocumentResolveOperations> = None;
|
||||
let client_operations: DeltaTextOperations;
|
||||
|
||||
if read_guard.is_empty() {
|
||||
// Do nothing
|
||||
@ -100,11 +100,11 @@ impl EditDocumentQueue {
|
||||
} else {
|
||||
let (s_prime, c_prime) = read_guard.get_operations().transform(&operations)?;
|
||||
client_operations = c_prime;
|
||||
server_operations = Some(DocumentResolveOperations(s_prime));
|
||||
server_operations = Some(DeltaDocumentResolveOperations(s_prime));
|
||||
}
|
||||
drop(read_guard);
|
||||
Ok::<TextTransformOperations, CollaborateError>(TransformOperations {
|
||||
client_operations: DocumentResolveOperations(client_operations),
|
||||
client_operations: DeltaDocumentResolveOperations(client_operations),
|
||||
server_operations,
|
||||
})
|
||||
};
|
||||
@ -174,7 +174,7 @@ impl EditDocumentQueue {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_local_operations(&self, operations: TextOperations, md5: String) -> Result<RevId, FlowyError> {
|
||||
async fn save_local_operations(&self, operations: DeltaTextOperations, md5: String) -> Result<RevId, FlowyError> {
|
||||
let bytes = operations.json_bytes();
|
||||
let (base_rev_id, rev_id) = self.rev_manager.next_rev_id_pair();
|
||||
let user_id = self.user.user_id()?;
|
||||
@ -184,26 +184,26 @@ impl EditDocumentQueue {
|
||||
}
|
||||
}
|
||||
|
||||
pub type TextTransformOperations = TransformOperations<DocumentResolveOperations>;
|
||||
pub type TextTransformOperations = TransformOperations<DeltaDocumentResolveOperations>;
|
||||
pub(crate) type EditorCommandSender = Sender<EditorCommand>;
|
||||
pub(crate) type EditorCommandReceiver = Receiver<EditorCommand>;
|
||||
pub(crate) type Ret<T> = oneshot::Sender<Result<T, CollaborateError>>;
|
||||
|
||||
pub(crate) enum EditorCommand {
|
||||
ComposeLocalOperations {
|
||||
operations: TextOperations,
|
||||
operations: DeltaTextOperations,
|
||||
ret: Ret<()>,
|
||||
},
|
||||
ComposeRemoteOperation {
|
||||
client_operations: TextOperations,
|
||||
client_operations: DeltaTextOperations,
|
||||
ret: Ret<OperationsMD5>,
|
||||
},
|
||||
ResetOperations {
|
||||
operations: TextOperations,
|
||||
operations: DeltaTextOperations,
|
||||
ret: Ret<OperationsMD5>,
|
||||
},
|
||||
TransformOperations {
|
||||
operations: TextOperations,
|
||||
operations: DeltaTextOperations,
|
||||
ret: Ret<TextTransformOperations>,
|
||||
},
|
||||
Insert {
|
||||
@ -242,7 +242,7 @@ pub(crate) enum EditorCommand {
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
GetOperations {
|
||||
ret: Ret<TextOperations>,
|
||||
ret: Ret<DeltaTextOperations>,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -13,33 +13,35 @@ use flowy_sync::{
|
||||
errors::CollaborateResult,
|
||||
};
|
||||
use lib_infra::future::{BoxResultFuture, FutureResult};
|
||||
use lib_ot::text_delta::TextOperations;
|
||||
use lib_ot::text_delta::DeltaTextOperations;
|
||||
use lib_ws::WSConnectState;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::{broadcast, oneshot};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DocumentResolveOperations(pub TextOperations);
|
||||
pub struct DeltaDocumentResolveOperations(pub DeltaTextOperations);
|
||||
|
||||
impl OperationsDeserializer<DocumentResolveOperations> for DocumentResolveOperations {
|
||||
fn deserialize_revisions(revisions: Vec<Revision>) -> FlowyResult<DocumentResolveOperations> {
|
||||
Ok(DocumentResolveOperations(make_operations_from_revisions(revisions)?))
|
||||
impl OperationsDeserializer<DeltaDocumentResolveOperations> for DeltaDocumentResolveOperations {
|
||||
fn deserialize_revisions(revisions: Vec<Revision>) -> FlowyResult<DeltaDocumentResolveOperations> {
|
||||
Ok(DeltaDocumentResolveOperations(make_operations_from_revisions(
|
||||
revisions,
|
||||
)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl OperationsSerializer for DocumentResolveOperations {
|
||||
impl OperationsSerializer for DeltaDocumentResolveOperations {
|
||||
fn serialize_operations(&self) -> Bytes {
|
||||
self.0.json_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentResolveOperations {
|
||||
pub fn into_inner(self) -> TextOperations {
|
||||
impl DeltaDocumentResolveOperations {
|
||||
pub fn into_inner(self) -> DeltaTextOperations {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub type DocumentConflictController = ConflictController<DocumentResolveOperations>;
|
||||
pub type DocumentConflictController = ConflictController<DeltaDocumentResolveOperations>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn make_document_ws_manager(
|
||||
@ -129,8 +131,11 @@ struct DocumentConflictResolver {
|
||||
edit_cmd_tx: EditorCommandSender,
|
||||
}
|
||||
|
||||
impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
|
||||
fn compose_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||
impl ConflictResolver<DeltaDocumentResolveOperations> for DocumentConflictResolver {
|
||||
fn compose_operations(
|
||||
&self,
|
||||
operations: DeltaDocumentResolveOperations,
|
||||
) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||
let tx = self.edit_cmd_tx.clone();
|
||||
let operations = operations.into_inner();
|
||||
Box::pin(async move {
|
||||
@ -150,8 +155,8 @@ impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
|
||||
|
||||
fn transform_operations(
|
||||
&self,
|
||||
operations: DocumentResolveOperations,
|
||||
) -> BoxResultFuture<TransformOperations<DocumentResolveOperations>, FlowyError> {
|
||||
operations: DeltaDocumentResolveOperations,
|
||||
) -> BoxResultFuture<TransformOperations<DeltaDocumentResolveOperations>, FlowyError> {
|
||||
let tx = self.edit_cmd_tx.clone();
|
||||
let operations = operations.into_inner();
|
||||
Box::pin(async move {
|
||||
@ -166,7 +171,10 @@ impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||
fn reset_operations(
|
||||
&self,
|
||||
operations: DeltaDocumentResolveOperations,
|
||||
) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||
let tx = self.edit_cmd_tx.clone();
|
||||
let operations = operations.into_inner();
|
||||
Box::pin(async move {
|
||||
|
75
frontend/rust-lib/flowy-document/src/services/migration.rs
Normal file
75
frontend/rust-lib/flowy-document/src/services/migration.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use crate::editor::DeltaRevisionMigration;
|
||||
use crate::DocumentDatabase;
|
||||
use bytes::Bytes;
|
||||
use flowy_database::kv::KV;
|
||||
use flowy_error::FlowyResult;
|
||||
use flowy_revision::disk::{DeltaRevisionSql, RevisionDiskCache, RevisionRecord, SQLiteDocumentRevisionPersistence};
|
||||
use flowy_sync::entities::revision::{md5, Revision};
|
||||
use flowy_sync::util::make_operations_from_revisions;
|
||||
use std::sync::Arc;
|
||||
|
||||
const V1_MIGRATION: &str = "DOCUMENT_V1_MIGRATION";
|
||||
pub(crate) struct DocumentMigration {
|
||||
user_id: String,
|
||||
database: Arc<dyn DocumentDatabase>,
|
||||
}
|
||||
|
||||
impl DocumentMigration {
|
||||
pub fn new(user_id: &str, database: Arc<dyn DocumentDatabase>) -> Self {
|
||||
let user_id = user_id.to_owned();
|
||||
Self { user_id, database }
|
||||
}
|
||||
|
||||
pub fn run_v1_migration(&self) -> FlowyResult<()> {
|
||||
let key = migration_flag_key(&self.user_id, V1_MIGRATION);
|
||||
if KV::get_bool(&key) {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let pool = self.database.db_pool()?;
|
||||
let conn = &*pool.get()?;
|
||||
let disk_cache = SQLiteDocumentRevisionPersistence::new(&self.user_id, pool);
|
||||
let documents = DeltaRevisionSql::read_all_documents(&self.user_id, conn)?;
|
||||
tracing::info!("[Document Migration]: try migrate {} documents", documents.len());
|
||||
for revisions in documents {
|
||||
if revisions.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let document_id = revisions.first().unwrap().object_id.clone();
|
||||
match make_operations_from_revisions(revisions) {
|
||||
Ok(delta) => match DeltaRevisionMigration::run(delta) {
|
||||
Ok(transaction) => {
|
||||
let bytes = Bytes::from(transaction.to_bytes()?);
|
||||
let md5 = format!("{:x}", md5::compute(&bytes));
|
||||
let revision = Revision::new(&document_id, 0, 1, bytes, &self.user_id, md5);
|
||||
let record = RevisionRecord::new(revision);
|
||||
match disk_cache.create_revision_records(vec![record]) {
|
||||
Ok(_) => {}
|
||||
Err(err) => {
|
||||
tracing::error!("[Document Migration]: Save revisions to disk failed {:?}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::error!(
|
||||
"[Document Migration]: Migrate revisions to transaction failed {:?}",
|
||||
err
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("[Document migration]: Make delta from revisions failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
|
||||
KV::set_bool(&key, true);
|
||||
tracing::info!("Run document v1 migration");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
fn migration_flag_key(user_id: &str, version: &str) -> String {
|
||||
md5(format!("{}{}", user_id, version,))
|
||||
}
|
4
frontend/rust-lib/flowy-document/src/services/mod.rs
Normal file
4
frontend/rust-lib/flowy-document/src/services/mod.rs
Normal file
@ -0,0 +1,4 @@
|
||||
mod migration;
|
||||
mod persistence;
|
||||
|
||||
pub use persistence::*;
|
23
frontend/rust-lib/flowy-document/src/services/persistence.rs
Normal file
23
frontend/rust-lib/flowy-document/src/services/persistence.rs
Normal file
@ -0,0 +1,23 @@
|
||||
use crate::services::migration::DocumentMigration;
|
||||
use crate::DocumentDatabase;
|
||||
use flowy_error::FlowyResult;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub struct DocumentPersistence {
|
||||
pub database: Arc<dyn DocumentDatabase>,
|
||||
}
|
||||
|
||||
impl DocumentPersistence {
|
||||
pub fn new(database: Arc<dyn DocumentDatabase>) -> Self {
|
||||
Self { database }
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip_all, err)]
|
||||
pub fn initialize(&self, user_id: &str) -> FlowyResult<()> {
|
||||
let migration = DocumentMigration::new(user_id, self.database.clone());
|
||||
if let Err(e) = migration.run_v1_migration() {
|
||||
tracing::error!("[Document Migration]: run v1 migration failed: {:?}", e);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
@ -3,7 +3,7 @@ use crate::editor::{TestBuilder, TestOp::*};
|
||||
use flowy_sync::client_document::{NewlineDocument, EmptyDocument};
|
||||
use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use lib_ot::text_delta::TextOperations;
|
||||
use lib_ot::text_delta::DeltaTextOperations;
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added() {
|
||||
@ -29,7 +29,7 @@ fn attributes_bold_added_and_invert_all() {
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#),
|
||||
Bold(0, Interval::new(0, 3), false),
|
||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":false}}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
||||
}
|
||||
@ -41,7 +41,7 @@ fn attributes_bold_added_and_invert_partial_suffix() {
|
||||
Bold(0, Interval::new(0, 4), true),
|
||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||
Bold(0, Interval::new(2, 4), false),
|
||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#),
|
||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
||||
}
|
||||
@ -53,7 +53,7 @@ fn attributes_bold_added_and_invert_partial_suffix2() {
|
||||
Bold(0, Interval::new(0, 4), true),
|
||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||
Bold(0, Interval::new(2, 4), false),
|
||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#),
|
||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34","attributes":{"bold":false}}]"#),
|
||||
Bold(0, Interval::new(2, 4), true),
|
||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
@ -95,7 +95,7 @@ fn attributes_bold_added_and_invert_partial_prefix() {
|
||||
Bold(0, Interval::new(0, 4), true),
|
||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||
Bold(0, Interval::new(0, 2), false),
|
||||
AssertDocJson(0, r#"[{"insert":"12"},{"insert":"34","attributes":{"bold":true}}]"#),
|
||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":false}},{"insert":"34","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDocument>(ops);
|
||||
}
|
||||
@ -762,12 +762,12 @@ fn attributes_preserve_list_format_on_merge() {
|
||||
|
||||
#[test]
|
||||
fn delta_compose() {
|
||||
let mut delta = TextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap();
|
||||
let mut delta = DeltaTextOperations::from_json(r#"[{"insert":"\n"}]"#).unwrap();
|
||||
let deltas = vec![
|
||||
TextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(),
|
||||
TextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(),
|
||||
TextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(),
|
||||
TextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(),
|
||||
DeltaTextOperations::from_json(r#"[{"retain":1,"attributes":{"list":"unchecked"}}]"#).unwrap(),
|
||||
DeltaTextOperations::from_json(r#"[{"insert":"a"}]"#).unwrap(),
|
||||
DeltaTextOperations::from_json(r#"[{"retain":1},{"insert":"\n","attributes":{"list":"unchecked"}}]"#).unwrap(),
|
||||
DeltaTextOperations::from_json(r#"[{"retain":2},{"retain":1,"attributes":{"list":""}}]"#).unwrap(),
|
||||
];
|
||||
|
||||
for d in deltas {
|
||||
|
@ -8,7 +8,7 @@ use derive_more::Display;
|
||||
use flowy_sync::client_document::{ClientDocument, InitialDocument};
|
||||
use lib_ot::{
|
||||
core::*,
|
||||
text_delta::{BuildInTextAttribute, TextOperations},
|
||||
text_delta::{BuildInTextAttribute, DeltaTextOperations},
|
||||
};
|
||||
use rand::{prelude::*, Rng as WrappedRng};
|
||||
use std::{sync::Once, time::Duration};
|
||||
@ -81,8 +81,8 @@ pub enum TestOp {
|
||||
|
||||
pub struct TestBuilder {
|
||||
documents: Vec<ClientDocument>,
|
||||
deltas: Vec<Option<TextOperations>>,
|
||||
primes: Vec<Option<TextOperations>>,
|
||||
deltas: Vec<Option<DeltaTextOperations>>,
|
||||
primes: Vec<Option<DeltaTextOperations>>,
|
||||
}
|
||||
|
||||
impl TestBuilder {
|
||||
@ -226,20 +226,20 @@ impl TestBuilder {
|
||||
|
||||
TestOp::AssertDocJson(delta_i, expected) => {
|
||||
let delta_json = self.documents[*delta_i].get_operations_json();
|
||||
let expected_delta: TextOperations = serde_json::from_str(expected).unwrap();
|
||||
let target_delta: TextOperations = serde_json::from_str(&delta_json).unwrap();
|
||||
let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap();
|
||||
let target_delta: DeltaTextOperations = serde_json::from_str(&delta_json).unwrap();
|
||||
|
||||
if expected_delta != target_delta {
|
||||
log::error!("✅ expect: {}", expected,);
|
||||
log::error!("❌ receive: {}", delta_json);
|
||||
println!("✅ expect: {}", expected,);
|
||||
println!("❌ receive: {}", delta_json);
|
||||
}
|
||||
assert_eq!(target_delta, expected_delta);
|
||||
}
|
||||
|
||||
TestOp::AssertPrimeJson(doc_i, expected) => {
|
||||
let prime_json = self.primes[*doc_i].as_ref().unwrap().json_str();
|
||||
let expected_prime: TextOperations = serde_json::from_str(expected).unwrap();
|
||||
let target_prime: TextOperations = serde_json::from_str(&prime_json).unwrap();
|
||||
let expected_prime: DeltaTextOperations = serde_json::from_str(expected).unwrap();
|
||||
let target_prime: DeltaTextOperations = serde_json::from_str(&prime_json).unwrap();
|
||||
|
||||
if expected_prime != target_prime {
|
||||
log::error!("✅ expect prime: {}", expected,);
|
||||
@ -297,8 +297,8 @@ impl Rng {
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn gen_delta(&mut self, s: &str) -> TextOperations {
|
||||
let mut delta = TextOperations::default();
|
||||
pub fn gen_delta(&mut self, s: &str) -> DeltaTextOperations {
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let s = OTString::from(s);
|
||||
loop {
|
||||
let left = s.utf16_len() - delta.utf16_base_len;
|
||||
|
@ -1,8 +1,8 @@
|
||||
#![allow(clippy::all)]
|
||||
use crate::editor::{Rng, TestBuilder, TestOp::*};
|
||||
use flowy_sync::client_document::{EmptyDocument, NewlineDocument};
|
||||
use lib_ot::text_delta::TextOperationBuilder;
|
||||
use lib_ot::{core::Interval, core::*, text_delta::TextOperations};
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
use lib_ot::{core::Interval, core::*, text_delta::DeltaTextOperations};
|
||||
|
||||
#[test]
|
||||
fn attributes_insert_text() {
|
||||
@ -37,7 +37,7 @@ fn attributes_insert_text_at_middle() {
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_1() {
|
||||
let operations = OperationsBuilder::new().insert("123").insert("4").build();
|
||||
let delta = TextOperationBuilder::from_operations(operations);
|
||||
let delta = DeltaTextOperationBuilder::from_operations(operations);
|
||||
|
||||
let mut iterator = OperationIterator::from_interval(&delta, Interval::new(0, 4));
|
||||
assert_eq!(iterator.ops(), delta.ops);
|
||||
@ -45,7 +45,7 @@ fn delta_get_ops_in_interval_1() {
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_2() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("123");
|
||||
let insert_b = DeltaOperation::insert("4");
|
||||
let insert_c = DeltaOperation::insert("5");
|
||||
@ -89,7 +89,7 @@ fn delta_get_ops_in_interval_2() {
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_3() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("123456");
|
||||
delta.add(insert_a.clone());
|
||||
assert_eq!(
|
||||
@ -100,7 +100,7 @@ fn delta_get_ops_in_interval_3() {
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_4() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12");
|
||||
let insert_b = DeltaOperation::insert("34");
|
||||
let insert_c = DeltaOperation::insert("56");
|
||||
@ -130,7 +130,7 @@ fn delta_get_ops_in_interval_4() {
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_5() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("123456");
|
||||
let insert_b = DeltaOperation::insert("789");
|
||||
delta.ops.push(insert_a.clone());
|
||||
@ -148,7 +148,7 @@ fn delta_get_ops_in_interval_5() {
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_6() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12345678");
|
||||
delta.add(insert_a.clone());
|
||||
assert_eq!(
|
||||
@ -159,7 +159,7 @@ fn delta_get_ops_in_interval_6() {
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_7() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12345");
|
||||
let retain_a = DeltaOperation::retain(3);
|
||||
|
||||
@ -179,7 +179,7 @@ fn delta_get_ops_in_interval_7() {
|
||||
|
||||
#[test]
|
||||
fn delta_op_seek() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12345");
|
||||
let retain_a = DeltaOperation::retain(3);
|
||||
delta.add(insert_a.clone());
|
||||
@ -191,7 +191,7 @@ fn delta_op_seek() {
|
||||
|
||||
#[test]
|
||||
fn delta_utf16_code_unit_seek() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
@ -201,7 +201,7 @@ fn delta_utf16_code_unit_seek() {
|
||||
|
||||
#[test]
|
||||
fn delta_utf16_code_unit_seek_with_attributes() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
let attributes = AttributeBuilder::new()
|
||||
.insert("bold", true)
|
||||
.insert("italic", true)
|
||||
@ -221,7 +221,7 @@ fn delta_utf16_code_unit_seek_with_attributes() {
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("12"));
|
||||
@ -232,7 +232,7 @@ fn delta_next_op_len() {
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_with_chinese() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("你好"));
|
||||
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
@ -242,7 +242,7 @@ fn delta_next_op_len_with_chinese() {
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_with_english() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("ab"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 2);
|
||||
@ -251,7 +251,7 @@ fn delta_next_op_len_with_english() {
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_after_seek() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 5);
|
||||
@ -264,7 +264,7 @@ fn delta_next_op_len_after_seek() {
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_none() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
|
||||
@ -275,7 +275,7 @@ fn delta_next_op_len_none() {
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_with_len_zero() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_with_len(0), None,);
|
||||
@ -284,7 +284,7 @@ fn delta_next_op_with_len_zero() {
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_with_len_cross_op_return_last() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
delta.add(DeltaOperation::retain(1));
|
||||
delta.add(DeltaOperation::insert("678"));
|
||||
@ -297,7 +297,7 @@ fn delta_next_op_with_len_cross_op_return_last() {
|
||||
|
||||
#[test]
|
||||
fn lengths() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
assert_eq!(delta.utf16_base_len, 0);
|
||||
assert_eq!(delta.utf16_target_len, 0);
|
||||
delta.retain(5, AttributeHashMap::default());
|
||||
@ -315,7 +315,7 @@ fn lengths() {
|
||||
}
|
||||
#[test]
|
||||
fn sequence() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.retain(5, AttributeHashMap::default());
|
||||
delta.retain(0, AttributeHashMap::default());
|
||||
delta.insert("appflowy", AttributeHashMap::default());
|
||||
@ -348,7 +348,7 @@ fn apply_test() {
|
||||
|
||||
#[test]
|
||||
fn base_len_test() {
|
||||
let mut delta_a = TextOperations::default();
|
||||
let mut delta_a = DeltaTextOperations::default();
|
||||
delta_a.insert("a", AttributeHashMap::default());
|
||||
delta_a.insert("b", AttributeHashMap::default());
|
||||
delta_a.insert("c", AttributeHashMap::default());
|
||||
@ -387,7 +387,7 @@ fn invert_test() {
|
||||
|
||||
#[test]
|
||||
fn empty_ops() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.retain(0, AttributeHashMap::default());
|
||||
delta.insert("", AttributeHashMap::default());
|
||||
delta.delete(0);
|
||||
@ -395,12 +395,12 @@ fn empty_ops() {
|
||||
}
|
||||
#[test]
|
||||
fn eq() {
|
||||
let mut delta_a = TextOperations::default();
|
||||
let mut delta_a = DeltaTextOperations::default();
|
||||
delta_a.delete(1);
|
||||
delta_a.insert("lo", AttributeHashMap::default());
|
||||
delta_a.retain(2, AttributeHashMap::default());
|
||||
delta_a.retain(3, AttributeHashMap::default());
|
||||
let mut delta_b = TextOperations::default();
|
||||
let mut delta_b = DeltaTextOperations::default();
|
||||
delta_b.delete(1);
|
||||
delta_b.insert("l", AttributeHashMap::default());
|
||||
delta_b.insert("o", AttributeHashMap::default());
|
||||
@ -412,7 +412,7 @@ fn eq() {
|
||||
}
|
||||
#[test]
|
||||
fn ops_merging() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
assert_eq!(delta.ops.len(), 0);
|
||||
delta.retain(2, AttributeHashMap::default());
|
||||
assert_eq!(delta.ops.len(), 1);
|
||||
@ -436,7 +436,7 @@ fn ops_merging() {
|
||||
|
||||
#[test]
|
||||
fn is_noop() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
assert!(delta.is_noop());
|
||||
delta.retain(5, AttributeHashMap::default());
|
||||
assert!(delta.is_noop());
|
||||
@ -484,13 +484,13 @@ fn transform_random_delta() {
|
||||
|
||||
#[test]
|
||||
fn transform_with_two_delta() {
|
||||
let mut a = TextOperations::default();
|
||||
let mut a = DeltaTextOperations::default();
|
||||
let mut a_s = String::new();
|
||||
a.insert("123", AttributeBuilder::new().insert("bold", true).build());
|
||||
a_s = a.apply(&a_s).unwrap();
|
||||
assert_eq!(&a_s, "123");
|
||||
|
||||
let mut b = TextOperations::default();
|
||||
let mut b = DeltaTextOperations::default();
|
||||
let mut b_s = String::new();
|
||||
b.insert("456", AttributeHashMap::default());
|
||||
b_s = b.apply(&b_s).unwrap();
|
||||
@ -580,10 +580,10 @@ fn transform_two_conflict_non_seq_delta() {
|
||||
|
||||
#[test]
|
||||
fn delta_invert_no_attribute_delta() {
|
||||
let mut delta = TextOperations::default();
|
||||
let mut delta = DeltaTextOperations::default();
|
||||
delta.add(DeltaOperation::insert("123"));
|
||||
|
||||
let mut change = TextOperations::default();
|
||||
let mut change = DeltaTextOperations::default();
|
||||
change.add(DeltaOperation::retain(3));
|
||||
change.add(DeltaOperation::insert("456"));
|
||||
let undo = change.invert(&delta);
|
||||
|
@ -1,8 +1,8 @@
|
||||
use flowy_sync::client_document::{ClientDocument, EmptyDocument};
|
||||
use lib_ot::text_delta::TextOperation;
|
||||
use lib_ot::text_delta::DeltaTextOperation;
|
||||
use lib_ot::{
|
||||
core::*,
|
||||
text_delta::{BuildInTextAttribute, TextOperations},
|
||||
text_delta::{BuildInTextAttribute, DeltaTextOperations},
|
||||
};
|
||||
|
||||
#[test]
|
||||
@ -15,7 +15,7 @@ fn operation_insert_serialize_test() {
|
||||
let json = serde_json::to_string(&operation).unwrap();
|
||||
eprintln!("{}", json);
|
||||
|
||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
||||
let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(insert_op, operation);
|
||||
}
|
||||
|
||||
@ -24,15 +24,15 @@ fn operation_retain_serialize_test() {
|
||||
let operation = DeltaOperation::Retain(12.into());
|
||||
let json = serde_json::to_string(&operation).unwrap();
|
||||
eprintln!("{}", json);
|
||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
||||
let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(insert_op, operation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_delete_serialize_test() {
|
||||
let operation = TextOperation::Delete(2);
|
||||
let operation = DeltaTextOperation::Delete(2);
|
||||
let json = serde_json::to_string(&operation).unwrap();
|
||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
||||
let insert_op: DeltaTextOperation = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(insert_op, operation);
|
||||
}
|
||||
|
||||
@ -77,7 +77,7 @@ fn delta_deserialize_test() {
|
||||
{"retain":2,"attributes":{"italic":true,"bold":true}},
|
||||
{"retain":2,"attributes":{"italic":true,"bold":true}}
|
||||
]"#;
|
||||
let delta = TextOperations::from_json(json).unwrap();
|
||||
let delta = DeltaTextOperations::from_json(json).unwrap();
|
||||
eprintln!("{}", delta);
|
||||
}
|
||||
|
||||
@ -86,12 +86,12 @@ fn delta_deserialize_null_test() {
|
||||
let json = r#"[
|
||||
{"retain":7,"attributes":{"bold":null}}
|
||||
]"#;
|
||||
let delta1 = TextOperations::from_json(json).unwrap();
|
||||
let delta1 = DeltaTextOperations::from_json(json).unwrap();
|
||||
|
||||
let mut attribute = BuildInTextAttribute::Bold(true);
|
||||
attribute.remove_value();
|
||||
|
||||
let delta2 = OperationBuilder::new()
|
||||
let delta2 = DeltaOperationBuilder::new()
|
||||
.retain_with_attributes(7, attribute.into())
|
||||
.build();
|
||||
|
||||
|
@ -0,0 +1,24 @@
|
||||
use crate::new_document::script::DocumentEditorTest;
|
||||
use crate::new_document::script::EditScript::*;
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_insert_h1_style_test() {
|
||||
let scripts = vec![
|
||||
ComposeTransactionStr {
|
||||
transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"insert":"/"}],"inverted":[{"delete":1}]}],"after_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#,
|
||||
},
|
||||
AssertContent {
|
||||
expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"/"}]}]}}"#,
|
||||
},
|
||||
ComposeTransactionStr {
|
||||
transaction: r#"{"operations":[{"op":"update_text","path":[0,0],"delta":[{"delete":1}],"inverted":[{"insert":"/"}]}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":1},"end":{"path":[0,0],"offset":1}}}"#,
|
||||
},
|
||||
ComposeTransactionStr {
|
||||
transaction: r#"{"operations":[{"op":"update","path":[0,0],"attributes":{"subtype":"heading","heading":"h1"},"oldAttributes":{"subtype":null,"heading":null}}],"after_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}},"before_selection":{"start":{"path":[0,0],"offset":0},"end":{"path":[0,0],"offset":0}}}"#,
|
||||
},
|
||||
AssertContent {
|
||||
expected: r#"{"document":{"type":"editor","children":[{"type":"text","attributes":{"subtype":"heading","heading":"h1"}}]}}"#,
|
||||
},
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
mod document_compose_test;
|
||||
mod script;
|
||||
mod test;
|
||||
|
@ -1,17 +1,37 @@
|
||||
use flowy_document::editor::AppFlowyDocumentEditor;
|
||||
use flowy_document::editor::{AppFlowyDocumentEditor, Document, DocumentTransaction};
|
||||
|
||||
use flowy_document::entities::DocumentVersionPB;
|
||||
use flowy_test::helper::ViewTest;
|
||||
use flowy_test::FlowySDKTest;
|
||||
use lib_ot::core::{Body, Changeset, NodeDataBuilder, NodeOperation, Path, Transaction};
|
||||
use lib_ot::text_delta::TextOperations;
|
||||
use lib_ot::text_delta::DeltaTextOperations;
|
||||
use std::sync::Arc;
|
||||
|
||||
pub enum EditScript {
|
||||
InsertText { path: Path, delta: TextOperations },
|
||||
UpdateText { path: Path, delta: TextOperations },
|
||||
Delete { path: Path },
|
||||
AssertContent { expected: &'static str },
|
||||
AssertPrettyContent { expected: &'static str },
|
||||
InsertText {
|
||||
path: Path,
|
||||
delta: DeltaTextOperations,
|
||||
},
|
||||
UpdateText {
|
||||
path: Path,
|
||||
delta: DeltaTextOperations,
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
ComposeTransaction {
|
||||
transaction: Transaction,
|
||||
},
|
||||
ComposeTransactionStr {
|
||||
transaction: &'static str,
|
||||
},
|
||||
Delete {
|
||||
path: Path,
|
||||
},
|
||||
AssertContent {
|
||||
expected: &'static str,
|
||||
},
|
||||
AssertPrettyContent {
|
||||
expected: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct DocumentEditorTest {
|
||||
@ -21,7 +41,8 @@ pub struct DocumentEditorTest {
|
||||
|
||||
impl DocumentEditorTest {
|
||||
pub async fn new() -> Self {
|
||||
let sdk = FlowySDKTest::new(true);
|
||||
let version = DocumentVersionPB::V1;
|
||||
let sdk = FlowySDKTest::new(version.clone());
|
||||
let _ = sdk.init_user().await;
|
||||
|
||||
let test = ViewTest::new_document_view(&sdk).await;
|
||||
@ -62,6 +83,14 @@ impl DocumentEditorTest {
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
EditScript::ComposeTransaction { transaction } => {
|
||||
self.editor.apply_transaction(transaction).await.unwrap();
|
||||
}
|
||||
EditScript::ComposeTransactionStr { transaction } => {
|
||||
let document_transaction = serde_json::from_str::<DocumentTransaction>(transaction).unwrap();
|
||||
let transaction: Transaction = document_transaction.into();
|
||||
self.editor.apply_transaction(transaction).await.unwrap();
|
||||
}
|
||||
EditScript::Delete { path } => {
|
||||
let operation = NodeOperation::Delete { path, nodes: vec![] };
|
||||
self.editor
|
||||
@ -72,6 +101,9 @@ impl DocumentEditorTest {
|
||||
EditScript::AssertContent { expected } => {
|
||||
//
|
||||
let content = self.editor.get_content(false).await.unwrap();
|
||||
let expected_document: Document = serde_json::from_str(expected).unwrap();
|
||||
let expected = serde_json::to_string(&expected_document).unwrap();
|
||||
|
||||
assert_eq!(content, expected);
|
||||
}
|
||||
EditScript::AssertPrettyContent { expected } => {
|
||||
|
@ -1,7 +1,7 @@
|
||||
use crate::new_document::script::DocumentEditorTest;
|
||||
use crate::new_document::script::EditScript::*;
|
||||
|
||||
use lib_ot::text_delta::TextOperationBuilder;
|
||||
use lib_ot::text_delta::DeltaTextOperationBuilder;
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_initialize_test() {
|
||||
@ -13,7 +13,7 @@ async fn document_initialize_test() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn document_insert_text_test() {
|
||||
let delta = TextOperationBuilder::new().insert("Hello world").build();
|
||||
let delta = DeltaTextOperationBuilder::new().insert("Hello world").build();
|
||||
let expected = r#"{
|
||||
"document": {
|
||||
"type": "editor",
|
||||
@ -49,7 +49,7 @@ async fn document_update_text_test() {
|
||||
let scripts = vec![
|
||||
UpdateText {
|
||||
path: vec![0, 0].into(),
|
||||
delta: TextOperationBuilder::new().insert(&hello_world).build(),
|
||||
delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(),
|
||||
},
|
||||
AssertPrettyContent {
|
||||
expected: r#"{
|
||||
@ -75,7 +75,7 @@ async fn document_update_text_test() {
|
||||
let scripts = vec![
|
||||
UpdateText {
|
||||
path: vec![0, 0].into(),
|
||||
delta: TextOperationBuilder::new()
|
||||
delta: DeltaTextOperationBuilder::new()
|
||||
.retain(hello_world.len())
|
||||
.insert(", AppFlowy")
|
||||
.build(),
|
||||
@ -122,11 +122,11 @@ async fn document_delete_text_test() {
|
||||
let scripts = vec![
|
||||
UpdateText {
|
||||
path: vec![0, 0].into(),
|
||||
delta: TextOperationBuilder::new().insert(&hello_world).build(),
|
||||
delta: DeltaTextOperationBuilder::new().insert(&hello_world).build(),
|
||||
},
|
||||
UpdateText {
|
||||
path: vec![0, 0].into(),
|
||||
delta: TextOperationBuilder::new().retain(5).delete(6).build(),
|
||||
delta: DeltaTextOperationBuilder::new().retain(5).delete(6).build(),
|
||||
},
|
||||
AssertPrettyContent { expected },
|
||||
];
|
||||
@ -139,7 +139,7 @@ async fn document_delete_node_test() {
|
||||
let scripts = vec![
|
||||
UpdateText {
|
||||
path: vec![0, 0].into(),
|
||||
delta: TextOperationBuilder::new().insert("Hello world").build(),
|
||||
delta: DeltaTextOperationBuilder::new().insert("Hello world").build(),
|
||||
},
|
||||
AssertContent {
|
||||
expected: r#"{"document":{"type":"editor","children":[{"type":"text","delta":[{"insert":"Hello world"}]}]}}"#,
|
||||
|
@ -2,7 +2,7 @@ use flowy_document::old_editor::editor::DeltaDocumentEditor;
|
||||
use flowy_document::TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS;
|
||||
use flowy_revision::disk::RevisionState;
|
||||
use flowy_test::{helper::ViewTest, FlowySDKTest};
|
||||
use lib_ot::{core::Interval, text_delta::TextOperations};
|
||||
use lib_ot::{core::Interval, text_delta::DeltaTextOperations};
|
||||
use std::sync::Arc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
@ -75,7 +75,7 @@ impl DeltaDocumentEditorTest {
|
||||
assert_eq!(next_revision.rev_id, rev_id.unwrap());
|
||||
}
|
||||
EditorScript::AssertJson(expected) => {
|
||||
let expected_delta: TextOperations = serde_json::from_str(expected).unwrap();
|
||||
let expected_delta: DeltaTextOperations = serde_json::from_str(expected).unwrap();
|
||||
let delta = self.editor.document_operations().await.unwrap();
|
||||
if expected_delta != delta {
|
||||
eprintln!("✅ expect: {}", expected,);
|
||||
|
Reference in New Issue
Block a user