mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Refactor/rename crate (#1275)
This commit is contained in:
59
frontend/rust-lib/flowy-document/Cargo.toml
Normal file
59
frontend/rust-lib/flowy-document/Cargo.toml
Normal file
@ -0,0 +1,59 @@
|
||||
|
||||
[package]
|
||||
name = "flowy-document"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
flowy-sync = { path = "../../../shared-lib/flowy-sync"}
|
||||
flowy-derive = { path = "../../../shared-lib/flowy-derive" }
|
||||
lib-ot = { path = "../../../shared-lib/lib-ot" }
|
||||
lib-ws = { path = "../../../shared-lib/lib-ws" }
|
||||
lib-infra = { path = "../../../shared-lib/lib-infra" }
|
||||
|
||||
lib-dispatch = { path = "../lib-dispatch" }
|
||||
flowy-database = { path = "../flowy-database" }
|
||||
flowy-revision = { path = "../flowy-revision" }
|
||||
flowy-error = { path = "../flowy-error", features = ["collaboration", "ot", "http_server", "serde", "db"] }
|
||||
dart-notify = { path = "../dart-notify" }
|
||||
|
||||
diesel = {version = "1.4.8", features = ["sqlite"]}
|
||||
diesel_derives = {version = "1.4.1", features = ["sqlite"]}
|
||||
protobuf = {version = "2.18.0"}
|
||||
unicode-segmentation = "1.8"
|
||||
log = "0.4.14"
|
||||
tokio = {version = "1", features = ["sync"]}
|
||||
tracing = { version = "0.1", features = ["log"] }
|
||||
|
||||
bytes = { version = "1.1" }
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
dashmap = "5"
|
||||
url = "2.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = {version = "1.0"}
|
||||
chrono = "0.4.19"
|
||||
futures-util = "0.3.15"
|
||||
async-stream = "0.3.2"
|
||||
futures = "0.3.15"
|
||||
|
||||
[dev-dependencies]
|
||||
flowy-test = { path = "../flowy-test" }
|
||||
flowy-document = { path = "../flowy-document", features = ["flowy_unit_test"]}
|
||||
derive_more = {version = "0.99", features = ["display"]}
|
||||
tracing-subscriber = "0.2.0"
|
||||
|
||||
color-eyre = { version = "0.5", default-features = false }
|
||||
criterion = "0.3"
|
||||
rand = "0.8.5"
|
||||
|
||||
[build-dependencies]
|
||||
lib-infra = { path = "../../../shared-lib/lib-infra", features = ["protobuf_file_gen", "proto_gen"] }
|
||||
|
||||
[features]
|
||||
sync = []
|
||||
cloud_sync = ["sync"]
|
||||
flowy_unit_test = ["lib-ot/flowy_unit_test", "flowy-revision/flowy_unit_test"]
|
||||
dart = ["lib-infra/dart"]
|
3
frontend/rust-lib/flowy-document/Flowy.toml
Normal file
3
frontend/rust-lib/flowy-document/Flowy.toml
Normal file
@ -0,0 +1,3 @@
|
||||
# Check out the FlowyConfig (located in flowy_toml.rs) for more details.
|
||||
proto_input = ["src/event_map.rs", "src/entities.rs"]
|
||||
event_files = ["src/event_map.rs"]
|
9
frontend/rust-lib/flowy-document/build.rs
Normal file
9
frontend/rust-lib/flowy-document/build.rs
Normal file
@ -0,0 +1,9 @@
|
||||
use lib_infra::code_gen;
|
||||
|
||||
fn main() {
|
||||
let crate_name = env!("CARGO_PKG_NAME");
|
||||
code_gen::protobuf_file::gen(crate_name);
|
||||
|
||||
#[cfg(feature = "dart")]
|
||||
code_gen::dart_event::gen(crate_name);
|
||||
}
|
278
frontend/rust-lib/flowy-document/src/editor.rs
Normal file
278
frontend/rust-lib/flowy-document/src/editor.rs
Normal file
@ -0,0 +1,278 @@
|
||||
use crate::web_socket::EditorCommandSender;
|
||||
use crate::{
|
||||
errors::FlowyError,
|
||||
queue::{EditDocumentQueue, EditorCommand},
|
||||
DocumentUser,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use flowy_error::{internal_error, FlowyResult};
|
||||
use flowy_revision::{
|
||||
RevisionCloudService, RevisionCompress, RevisionManager, RevisionObjectDeserializer, RevisionObjectSerializer,
|
||||
RevisionWebSocket,
|
||||
};
|
||||
use flowy_sync::entities::ws_data::ServerRevisionWSData;
|
||||
use flowy_sync::{
|
||||
entities::{document::DocumentPayloadPB, revision::Revision},
|
||||
errors::CollaborateResult,
|
||||
util::make_operations_from_revisions,
|
||||
};
|
||||
use lib_ot::core::{AttributeEntry, AttributeHashMap};
|
||||
use lib_ot::{
|
||||
core::{DeltaOperation, Interval},
|
||||
text_delta::TextOperations,
|
||||
};
|
||||
use lib_ws::WSConnectState;
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
pub struct DocumentEditor {
|
||||
pub doc_id: String,
|
||||
#[allow(dead_code)]
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
#[cfg(feature = "sync")]
|
||||
ws_manager: Arc<flowy_revision::RevisionWebSocketManager>,
|
||||
edit_cmd_tx: EditorCommandSender,
|
||||
}
|
||||
|
||||
impl DocumentEditor {
|
||||
#[allow(unused_variables)]
|
||||
pub(crate) async fn new(
|
||||
doc_id: &str,
|
||||
user: Arc<dyn DocumentUser>,
|
||||
mut rev_manager: RevisionManager,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
cloud_service: Arc<dyn RevisionCloudService>,
|
||||
) -> FlowyResult<Arc<Self>> {
|
||||
let document_info = rev_manager.load::<DocumentRevisionSerde>(Some(cloud_service)).await?;
|
||||
let operations = TextOperations::from_bytes(&document_info.content)?;
|
||||
let rev_manager = Arc::new(rev_manager);
|
||||
let doc_id = doc_id.to_string();
|
||||
let user_id = user.user_id()?;
|
||||
|
||||
let edit_cmd_tx = spawn_edit_queue(user, rev_manager.clone(), operations);
|
||||
#[cfg(feature = "sync")]
|
||||
let ws_manager = crate::web_socket::make_document_ws_manager(
|
||||
doc_id.clone(),
|
||||
user_id.clone(),
|
||||
edit_cmd_tx.clone(),
|
||||
rev_manager.clone(),
|
||||
rev_web_socket,
|
||||
)
|
||||
.await;
|
||||
let editor = Arc::new(Self {
|
||||
doc_id,
|
||||
rev_manager,
|
||||
#[cfg(feature = "sync")]
|
||||
ws_manager,
|
||||
edit_cmd_tx,
|
||||
});
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
pub async fn insert<T: ToString>(&self, index: usize, data: T) -> Result<(), FlowyError> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||
let msg = EditorCommand::Insert {
|
||||
index,
|
||||
data: data.to_string(),
|
||||
ret,
|
||||
};
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete(&self, interval: Interval) -> Result<(), FlowyError> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||
let msg = EditorCommand::Delete { interval, ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn format(&self, interval: Interval, attribute: AttributeEntry) -> Result<(), FlowyError> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||
let msg = EditorCommand::Format {
|
||||
interval,
|
||||
attribute,
|
||||
ret,
|
||||
};
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn replace<T: ToString>(&self, interval: Interval, data: T) -> Result<(), FlowyError> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||
let msg = EditorCommand::Replace {
|
||||
interval,
|
||||
data: data.to_string(),
|
||||
ret,
|
||||
};
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn can_undo(&self) -> bool {
|
||||
let (ret, rx) = oneshot::channel::<bool>();
|
||||
let msg = EditorCommand::CanUndo { ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
rx.await.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn can_redo(&self) -> bool {
|
||||
let (ret, rx) = oneshot::channel::<bool>();
|
||||
let msg = EditorCommand::CanRedo { ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
rx.await.unwrap_or(false)
|
||||
}
|
||||
|
||||
pub async fn undo(&self) -> Result<(), FlowyError> {
|
||||
let (ret, rx) = oneshot::channel();
|
||||
let msg = EditorCommand::Undo { ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn redo(&self) -> Result<(), FlowyError> {
|
||||
let (ret, rx) = oneshot::channel();
|
||||
let msg = EditorCommand::Redo { ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_operation_str(&self) -> FlowyResult<String> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<String>>();
|
||||
let msg = EditorCommand::StringifyOperations { ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let json = rx.await.map_err(internal_error)??;
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, data), err)]
|
||||
pub(crate) async fn compose_local_operations(&self, data: Bytes) -> Result<(), FlowyError> {
|
||||
let operations = TextOperations::from_bytes(&data)?;
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<()>>();
|
||||
let msg = EditorCommand::ComposeLocalOperations { operations, ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let _ = rx.await.map_err(internal_error)??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
pub fn stop(&self) {
|
||||
self.ws_manager.stop();
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "sync"))]
|
||||
pub fn stop(&self) {}
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
pub(crate) async fn receive_ws_data(&self, data: ServerRevisionWSData) -> Result<(), FlowyError> {
|
||||
self.ws_manager.receive_ws_data(data).await
|
||||
}
|
||||
#[cfg(not(feature = "sync"))]
|
||||
pub(crate) async fn receive_ws_data(&self, _data: ServerRevisionWSData) -> Result<(), FlowyError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "sync")]
|
||||
pub(crate) fn receive_ws_state(&self, state: &WSConnectState) {
|
||||
self.ws_manager.connect_state_changed(state.clone());
|
||||
}
|
||||
#[cfg(not(feature = "sync"))]
|
||||
pub(crate) fn receive_ws_state(&self, _state: &WSConnectState) {}
|
||||
}
|
||||
|
||||
impl std::ops::Drop for DocumentEditor {
|
||||
fn drop(&mut self) {
|
||||
tracing::trace!("{} DocumentEditor was dropped", self.doc_id)
|
||||
}
|
||||
}
|
||||
|
||||
// The edit queue will exit after the EditorCommandSender was dropped.
|
||||
fn spawn_edit_queue(
|
||||
user: Arc<dyn DocumentUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
delta: TextOperations,
|
||||
) -> EditorCommandSender {
|
||||
let (sender, receiver) = mpsc::channel(1000);
|
||||
let edit_queue = EditDocumentQueue::new(user, rev_manager, delta, receiver);
|
||||
// We can use tokio::task::spawn_local here by using tokio::spawn_blocking.
|
||||
// https://github.com/tokio-rs/tokio/issues/2095
|
||||
// tokio::task::spawn_blocking(move || {
|
||||
// let rt = tokio::runtime::Handle::current();
|
||||
// rt.block_on(async {
|
||||
// let local = tokio::task::LocalSet::new();
|
||||
// local.run_until(edit_queue.run()).await;
|
||||
// });
|
||||
// });
|
||||
tokio::spawn(edit_queue.run());
|
||||
sender
|
||||
}
|
||||
|
||||
#[cfg(feature = "flowy_unit_test")]
|
||||
impl DocumentEditor {
|
||||
pub async fn document_operations(&self) -> FlowyResult<TextOperations> {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<TextOperations>>();
|
||||
let msg = EditorCommand::ReadOperations { ret };
|
||||
let _ = self.edit_cmd_tx.send(msg).await;
|
||||
let delta = rx.await.map_err(internal_error)??;
|
||||
Ok(delta)
|
||||
}
|
||||
|
||||
pub fn rev_manager(&self) -> Arc<RevisionManager> {
|
||||
self.rev_manager.clone()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DocumentRevisionSerde();
|
||||
impl RevisionObjectDeserializer for DocumentRevisionSerde {
|
||||
type Output = DocumentPayloadPB;
|
||||
|
||||
fn deserialize_revisions(object_id: &str, revisions: Vec<Revision>) -> FlowyResult<Self::Output> {
|
||||
let (base_rev_id, rev_id) = revisions.last().unwrap().pair_rev_id();
|
||||
let mut delta = make_operations_from_revisions(revisions)?;
|
||||
correct_delta(&mut delta);
|
||||
|
||||
Result::<DocumentPayloadPB, FlowyError>::Ok(DocumentPayloadPB {
|
||||
doc_id: object_id.to_owned(),
|
||||
content: delta.json_str(),
|
||||
rev_id,
|
||||
base_rev_id,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl RevisionObjectSerializer for DocumentRevisionSerde {
|
||||
fn serialize_revisions(revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||
let operations = make_operations_from_revisions::<AttributeHashMap>(revisions)?;
|
||||
Ok(operations.json_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DocumentRevisionCompactor();
|
||||
impl RevisionCompress for DocumentRevisionCompactor {
|
||||
fn serialize_revisions(&self, revisions: Vec<Revision>) -> FlowyResult<Bytes> {
|
||||
DocumentRevisionSerde::serialize_revisions(revisions)
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
if let Some(op) = delta.ops.last() {
|
||||
let op_data = op.get_data();
|
||||
if !op_data.ends_with('\n') {
|
||||
tracing::warn!("The document must end with newline. Correcting it by inserting newline op");
|
||||
delta.ops.push(DeltaOperation::Insert("\n".into()));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(op) = delta.ops.iter().find(|op| !op.is_insert()) {
|
||||
tracing::warn!("The document can only contains insert operations, but found {:?}", op);
|
||||
delta.ops.retain(|op| op.is_insert());
|
||||
}
|
||||
}
|
110
frontend/rust-lib/flowy-document/src/entities.rs
Normal file
110
frontend/rust-lib/flowy-document/src/entities.rs
Normal file
@ -0,0 +1,110 @@
|
||||
use crate::errors::ErrorCode;
|
||||
use flowy_derive::{ProtoBuf, ProtoBuf_Enum};
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[derive(PartialEq, Debug, ProtoBuf_Enum, Clone)]
|
||||
pub enum ExportType {
|
||||
Text = 0,
|
||||
Markdown = 1,
|
||||
Link = 2,
|
||||
}
|
||||
|
||||
impl std::default::Default for ExportType {
|
||||
fn default() -> Self {
|
||||
ExportType::Text
|
||||
}
|
||||
}
|
||||
|
||||
impl std::convert::From<i32> for ExportType {
|
||||
fn from(val: i32) -> Self {
|
||||
match val {
|
||||
0 => ExportType::Text,
|
||||
1 => ExportType::Markdown,
|
||||
2 => ExportType::Link,
|
||||
_ => {
|
||||
log::error!("Invalid export type: {}", val);
|
||||
ExportType::Text
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct EditPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub doc_id: String,
|
||||
|
||||
// Encode in JSON format
|
||||
#[pb(index = 2)]
|
||||
pub operations: String,
|
||||
|
||||
// Encode in JSON format
|
||||
#[pb(index = 3)]
|
||||
pub operations_str: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EditParams {
|
||||
pub doc_id: String,
|
||||
|
||||
// Encode in JSON format
|
||||
pub operations: String,
|
||||
|
||||
// Encode in JSON format
|
||||
pub operations_str: String,
|
||||
}
|
||||
|
||||
impl TryInto<EditParams> for EditPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<EditParams, Self::Error> {
|
||||
Ok(EditParams {
|
||||
doc_id: self.doc_id,
|
||||
operations: self.operations,
|
||||
operations_str: self.operations_str,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct DocumentSnapshotPB {
|
||||
#[pb(index = 1)]
|
||||
pub doc_id: String,
|
||||
|
||||
/// Encode in JSON format
|
||||
#[pb(index = 2)]
|
||||
pub snapshot: String,
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct ExportPayloadPB {
|
||||
#[pb(index = 1)]
|
||||
pub view_id: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub export_type: ExportType,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ExportParams {
|
||||
pub view_id: String,
|
||||
pub export_type: ExportType,
|
||||
}
|
||||
|
||||
impl TryInto<ExportParams> for ExportPayloadPB {
|
||||
type Error = ErrorCode;
|
||||
fn try_into(self) -> Result<ExportParams, Self::Error> {
|
||||
Ok(ExportParams {
|
||||
view_id: self.view_id,
|
||||
export_type: self.export_type,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, ProtoBuf)]
|
||||
pub struct ExportDataPB {
|
||||
#[pb(index = 1)]
|
||||
pub data: String,
|
||||
|
||||
#[pb(index = 2)]
|
||||
pub export_type: ExportType,
|
||||
}
|
43
frontend/rust-lib/flowy-document/src/event_handler.rs
Normal file
43
frontend/rust-lib/flowy-document/src/event_handler.rs
Normal file
@ -0,0 +1,43 @@
|
||||
use crate::entities::{DocumentSnapshotPB, EditParams, EditPayloadPB, ExportDataPB, ExportParams, ExportPayloadPB};
|
||||
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>,
|
||||
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.get_operation_str().await?;
|
||||
data_result(DocumentSnapshotPB {
|
||||
doc_id: document_id.into(),
|
||||
snapshot: operations_str,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn apply_edit_handler(
|
||||
data: Data<EditPayloadPB>,
|
||||
manager: AppData<Arc<DocumentManager>>,
|
||||
) -> Result<(), FlowyError> {
|
||||
let params: EditParams = data.into_inner().try_into()?;
|
||||
let _ = manager.apply_edit(params).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "debug", skip(data, manager), err)]
|
||||
pub(crate) async fn export_handler(
|
||||
data: Data<ExportPayloadPB>,
|
||||
manager: AppData<Arc<DocumentManager>>,
|
||||
) -> DataResult<ExportDataPB, FlowyError> {
|
||||
let params: ExportParams = data.into_inner().try_into()?;
|
||||
let editor = manager.open_document_editor(¶ms.view_id).await?;
|
||||
let operations_str = editor.get_operation_str().await?;
|
||||
data_result(ExportDataPB {
|
||||
data: operations_str,
|
||||
export_type: params.export_type,
|
||||
})
|
||||
}
|
30
frontend/rust-lib/flowy-document/src/event_map.rs
Normal file
30
frontend/rust-lib/flowy-document/src/event_map.rs
Normal file
@ -0,0 +1,30 @@
|
||||
use crate::event_handler::*;
|
||||
use crate::DocumentManager;
|
||||
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
||||
use lib_dispatch::prelude::Module;
|
||||
use std::sync::Arc;
|
||||
use strum_macros::Display;
|
||||
|
||||
pub fn create(document_manager: Arc<DocumentManager>) -> Module {
|
||||
let mut module = Module::new().name(env!("CARGO_PKG_NAME")).data(document_manager);
|
||||
|
||||
module = module
|
||||
.event(DocumentEvent::GetDocument, get_document_handler)
|
||||
.event(DocumentEvent::ApplyEdit, apply_edit_handler)
|
||||
.event(DocumentEvent::ExportDocument, export_handler);
|
||||
|
||||
module
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Display, Hash, ProtoBuf_Enum, Flowy_Event)]
|
||||
#[event_err = "FlowyError"]
|
||||
pub enum DocumentEvent {
|
||||
#[event(input = "DocumentIdPB", output = "DocumentSnapshotPB")]
|
||||
GetDocument = 0,
|
||||
|
||||
#[event(input = "EditPayloadPB")]
|
||||
ApplyEdit = 1,
|
||||
|
||||
#[event(input = "ExportPayloadPB", output = "ExportDataPB")]
|
||||
ExportDocument = 2,
|
||||
}
|
27
frontend/rust-lib/flowy-document/src/lib.rs
Normal file
27
frontend/rust-lib/flowy-document/src/lib.rs
Normal file
@ -0,0 +1,27 @@
|
||||
pub mod editor;
|
||||
mod entities;
|
||||
mod event_handler;
|
||||
pub mod event_map;
|
||||
pub mod manager;
|
||||
mod queue;
|
||||
mod web_socket;
|
||||
|
||||
pub mod protobuf;
|
||||
pub use manager::*;
|
||||
pub mod errors {
|
||||
pub use flowy_error::{internal_error, ErrorCode, FlowyError};
|
||||
}
|
||||
|
||||
pub const TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS: u64 = 1000;
|
||||
|
||||
use crate::errors::FlowyError;
|
||||
use flowy_sync::entities::document::{CreateDocumentParams, DocumentIdPB, DocumentPayloadPB, ResetDocumentParams};
|
||||
use lib_infra::future::FutureResult;
|
||||
|
||||
pub trait DocumentCloudService: Send + Sync {
|
||||
fn create_document(&self, token: &str, params: CreateDocumentParams) -> FutureResult<(), FlowyError>;
|
||||
|
||||
fn fetch_document(&self, token: &str, params: DocumentIdPB) -> FutureResult<Option<DocumentPayloadPB>, FlowyError>;
|
||||
|
||||
fn update_document_content(&self, token: &str, params: ResetDocumentParams) -> FutureResult<(), FlowyError>;
|
||||
}
|
262
frontend/rust-lib/flowy-document/src/manager.rs
Normal file
262
frontend/rust-lib/flowy-document/src/manager.rs
Normal file
@ -0,0 +1,262 @@
|
||||
use crate::editor::DocumentRevisionCompactor;
|
||||
use crate::entities::EditParams;
|
||||
use crate::{editor::DocumentEditor, 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::{
|
||||
RevisionCloudService, RevisionManager, RevisionPersistence, RevisionWebSocket, SQLiteRevisionSnapshotPersistence,
|
||||
};
|
||||
use flowy_sync::entities::{
|
||||
document::{DocumentIdPB, DocumentOperationsPB},
|
||||
revision::{md5, RepeatedRevision, Revision},
|
||||
ws_data::ServerRevisionWSData,
|
||||
};
|
||||
use lib_infra::future::FutureResult;
|
||||
use std::{convert::TryInto, sync::Arc};
|
||||
|
||||
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>;
|
||||
fn db_pool(&self) -> Result<Arc<ConnectionPool>, FlowyError>;
|
||||
}
|
||||
|
||||
pub struct DocumentManager {
|
||||
cloud_service: Arc<dyn DocumentCloudService>,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
editor_map: Arc<DocumentEditorMap>,
|
||||
user: Arc<dyn DocumentUser>,
|
||||
}
|
||||
|
||||
impl DocumentManager {
|
||||
pub fn new(
|
||||
cloud_service: Arc<dyn DocumentCloudService>,
|
||||
document_user: Arc<dyn DocumentUser>,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cloud_service,
|
||||
rev_web_socket,
|
||||
editor_map: Arc::new(DocumentEditorMap::new()),
|
||||
user: document_user,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(&self) -> FlowyResult<()> {
|
||||
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 open_document_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<Arc<DocumentEditor>, FlowyError> {
|
||||
let editor_id = editor_id.as_ref();
|
||||
tracing::Span::current().record("editor_id", &editor_id);
|
||||
self.get_document_editor(editor_id).await
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self, editor_id), fields(editor_id), err)]
|
||||
pub fn close_document_editor<T: AsRef<str>>(&self, editor_id: T) -> Result<(), FlowyError> {
|
||||
let editor_id = editor_id.as_ref();
|
||||
tracing::Span::current().record("editor_id", &editor_id);
|
||||
self.editor_map.remove(editor_id);
|
||||
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.get_operation_str().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_str))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
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()?;
|
||||
// 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.reset_object(revisions).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn receive_ws_data(&self, data: Bytes) {
|
||||
let result: Result<ServerRevisionWSData, protobuf::ProtobufError> = data.try_into();
|
||||
match result {
|
||||
Ok(data) => match self.editor_map.get(&data.object_id) {
|
||||
None => tracing::error!("Can't find any source handler for {:?}-{:?}", data.object_id, data.ty),
|
||||
Some(editor) => match editor.receive_ws_data(data).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::error!("{}", e),
|
||||
},
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("Document ws data parser failed: {:?}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentManager {
|
||||
/// Returns the `DocumentEditor`
|
||||
/// Initializes the document editor if it's not initialized yet. Otherwise, returns the opened
|
||||
/// editor.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `doc_id`: the id of the document
|
||||
///
|
||||
/// returns: Result<Arc<DocumentEditor>, FlowyError>
|
||||
///
|
||||
async fn get_document_editor(&self, doc_id: &str) -> FlowyResult<Arc<DocumentEditor>> {
|
||||
match self.editor_map.get(doc_id) {
|
||||
None => {
|
||||
let db_pool = self.user.db_pool()?;
|
||||
self.init_document_editor(doc_id, db_pool).await
|
||||
}
|
||||
Some(editor) => Ok(editor),
|
||||
}
|
||||
}
|
||||
|
||||
/// Initializes a document editor with the doc_id
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `doc_id`: the id of the document
|
||||
/// * `pool`: sqlite connection pool
|
||||
///
|
||||
/// returns: Result<Arc<DocumentEditor>, FlowyError>
|
||||
///
|
||||
#[tracing::instrument(level = "trace", skip(self, pool), err)]
|
||||
async fn init_document_editor(
|
||||
&self,
|
||||
doc_id: &str,
|
||||
pool: Arc<ConnectionPool>,
|
||||
) -> Result<Arc<DocumentEditor>, FlowyError> {
|
||||
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 = DocumentEditor::new(doc_id, user, rev_manager, self.rev_web_socket.clone(), cloud_service).await?;
|
||||
self.editor_map.insert(doc_id, &editor);
|
||||
Ok(editor)
|
||||
}
|
||||
|
||||
fn make_document_rev_manager(
|
||||
&self,
|
||||
doc_id: &str,
|
||||
pool: Arc<ConnectionPool>,
|
||||
) -> Result<RevisionManager, FlowyError> {
|
||||
let user_id = self.user.user_id()?;
|
||||
let disk_cache = SQLiteDocumentRevisionPersistence::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);
|
||||
let rev_compactor = DocumentRevisionCompactor();
|
||||
|
||||
Ok(RevisionManager::new(
|
||||
&user_id,
|
||||
doc_id,
|
||||
rev_persistence,
|
||||
rev_compactor,
|
||||
// history_persistence,
|
||||
snapshot_persistence,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
struct DocumentRevisionCloudService {
|
||||
token: String,
|
||||
server: Arc<dyn DocumentCloudService>,
|
||||
}
|
||||
|
||||
impl RevisionCloudService for DocumentRevisionCloudService {
|
||||
#[tracing::instrument(level = "trace", skip(self))]
|
||||
fn fetch_object(&self, user_id: &str, object_id: &str) -> FutureResult<Vec<Revision>, FlowyError> {
|
||||
let params: DocumentIdPB = object_id.to_string().into();
|
||||
let server = self.server.clone();
|
||||
let token = self.token.clone();
|
||||
let user_id = user_id.to_string();
|
||||
|
||||
FutureResult::new(async move {
|
||||
match server.fetch_document(&token, params).await? {
|
||||
None => Err(FlowyError::record_not_found().context("Remote doesn't have this document")),
|
||||
Some(payload) => {
|
||||
let bytes = Bytes::from(payload.content.clone());
|
||||
let doc_md5 = md5(&bytes);
|
||||
let revision = Revision::new(
|
||||
&payload.doc_id,
|
||||
payload.base_rev_id,
|
||||
payload.rev_id,
|
||||
bytes,
|
||||
&user_id,
|
||||
doc_md5,
|
||||
);
|
||||
Ok(vec![revision])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DocumentEditorMap {
|
||||
inner: DashMap<String, Arc<DocumentEditor>>,
|
||||
}
|
||||
|
||||
impl DocumentEditorMap {
|
||||
fn new() -> Self {
|
||||
Self { inner: DashMap::new() }
|
||||
}
|
||||
|
||||
pub(crate) fn insert(&self, editor_id: &str, doc: &Arc<DocumentEditor>) {
|
||||
if self.inner.contains_key(editor_id) {
|
||||
log::warn!("Doc:{} already exists in cache", editor_id);
|
||||
}
|
||||
self.inner.insert(editor_id.to_string(), doc.clone());
|
||||
}
|
||||
|
||||
pub(crate) fn get(&self, editor_id: &str) -> Option<Arc<DocumentEditor>> {
|
||||
Some(self.inner.get(editor_id)?.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn remove(&self, editor_id: &str) {
|
||||
if let Some(editor) = self.get(editor_id) {
|
||||
editor.stop()
|
||||
}
|
||||
self.inner.remove(editor_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(web_socket, handlers))]
|
||||
fn listen_ws_state_changed(web_socket: Arc<dyn RevisionWebSocket>, handlers: Arc<DocumentEditorMap>) {
|
||||
tokio::spawn(async move {
|
||||
let mut notify = web_socket.subscribe_state_changed().await;
|
||||
while let Ok(state) = notify.recv().await {
|
||||
handlers.inner.iter().for_each(|handler| {
|
||||
handler.receive_ws_state(&state);
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
267
frontend/rust-lib/flowy-document/src/queue.rs
Normal file
267
frontend/rust-lib/flowy-document/src/queue.rs
Normal file
@ -0,0 +1,267 @@
|
||||
use crate::web_socket::{DocumentResolveOperations, EditorCommandReceiver};
|
||||
use crate::DocumentUser;
|
||||
use async_stream::stream;
|
||||
use flowy_error::FlowyError;
|
||||
use flowy_revision::{OperationsMD5, RevisionManager, TransformOperations};
|
||||
use flowy_sync::{
|
||||
client_document::{history::UndoResult, ClientDocument},
|
||||
entities::revision::{RevId, Revision},
|
||||
errors::CollaborateError,
|
||||
};
|
||||
use futures::stream::StreamExt;
|
||||
use lib_ot::core::AttributeEntry;
|
||||
use lib_ot::{
|
||||
core::{Interval, OperationTransform},
|
||||
text_delta::TextOperations,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::{oneshot, RwLock};
|
||||
|
||||
// The EditorCommandQueue executes each command that will alter the document in
|
||||
// serial.
|
||||
pub(crate) struct EditDocumentQueue {
|
||||
document: Arc<RwLock<ClientDocument>>,
|
||||
user: Arc<dyn DocumentUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
receiver: Option<EditorCommandReceiver>,
|
||||
}
|
||||
|
||||
impl EditDocumentQueue {
|
||||
pub(crate) fn new(
|
||||
user: Arc<dyn DocumentUser>,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
operations: TextOperations,
|
||||
receiver: EditorCommandReceiver,
|
||||
) -> Self {
|
||||
let document = Arc::new(RwLock::new(ClientDocument::from_operations(operations)));
|
||||
Self {
|
||||
document,
|
||||
user,
|
||||
rev_manager,
|
||||
receiver: Some(receiver),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn run(mut self) {
|
||||
let mut receiver = self.receiver.take().expect("Should only call once");
|
||||
let stream = stream! {
|
||||
loop {
|
||||
match receiver.recv().await {
|
||||
Some(msg) => yield msg,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
};
|
||||
stream
|
||||
.for_each(|command| async {
|
||||
match self.handle_command(command).await {
|
||||
Ok(_) => {}
|
||||
Err(e) => tracing::debug!("[EditCommandQueue]: {}", e),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tracing::instrument(level = "trace", skip(self), err)]
|
||||
async fn handle_command(&self, command: EditorCommand) -> Result<(), FlowyError> {
|
||||
match command {
|
||||
EditorCommand::ComposeLocalOperations { operations, ret } => {
|
||||
let mut document = self.document.write().await;
|
||||
let _ = document.compose_operations(operations.clone())?;
|
||||
let md5 = document.md5();
|
||||
drop(document);
|
||||
let _ = self.save_local_operations(operations, md5).await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
EditorCommand::ComposeRemoteOperation { client_operations, ret } => {
|
||||
let mut document = self.document.write().await;
|
||||
let _ = document.compose_operations(client_operations.clone())?;
|
||||
let md5 = document.md5();
|
||||
drop(document);
|
||||
let _ = ret.send(Ok(md5));
|
||||
}
|
||||
EditorCommand::ResetOperations { operations, ret } => {
|
||||
let mut document = self.document.write().await;
|
||||
let _ = document.set_operations(operations);
|
||||
let md5 = document.md5();
|
||||
drop(document);
|
||||
let _ = ret.send(Ok(md5));
|
||||
}
|
||||
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;
|
||||
|
||||
if read_guard.is_empty() {
|
||||
// Do nothing
|
||||
client_operations = operations;
|
||||
} else {
|
||||
let (s_prime, c_prime) = read_guard.get_operations().transform(&operations)?;
|
||||
client_operations = c_prime;
|
||||
server_operations = Some(DocumentResolveOperations(s_prime));
|
||||
}
|
||||
drop(read_guard);
|
||||
Ok::<TextTransformOperations, CollaborateError>(TransformOperations {
|
||||
client_operations: DocumentResolveOperations(client_operations),
|
||||
server_operations,
|
||||
})
|
||||
};
|
||||
let _ = ret.send(f().await);
|
||||
}
|
||||
EditorCommand::Insert { index, data, ret } => {
|
||||
let mut write_guard = self.document.write().await;
|
||||
let operations = write_guard.insert(index, data)?;
|
||||
let md5 = write_guard.md5();
|
||||
let _ = self.save_local_operations(operations, md5).await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
EditorCommand::Delete { interval, ret } => {
|
||||
let mut write_guard = self.document.write().await;
|
||||
let operations = write_guard.delete(interval)?;
|
||||
let md5 = write_guard.md5();
|
||||
let _ = self.save_local_operations(operations, md5).await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
EditorCommand::Format {
|
||||
interval,
|
||||
attribute,
|
||||
ret,
|
||||
} => {
|
||||
let mut write_guard = self.document.write().await;
|
||||
let operations = write_guard.format(interval, attribute)?;
|
||||
let md5 = write_guard.md5();
|
||||
let _ = self.save_local_operations(operations, md5).await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
EditorCommand::Replace { interval, data, ret } => {
|
||||
let mut write_guard = self.document.write().await;
|
||||
let operations = write_guard.replace(interval, data)?;
|
||||
let md5 = write_guard.md5();
|
||||
let _ = self.save_local_operations(operations, md5).await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
EditorCommand::CanUndo { ret } => {
|
||||
let _ = ret.send(self.document.read().await.can_undo());
|
||||
}
|
||||
EditorCommand::CanRedo { ret } => {
|
||||
let _ = ret.send(self.document.read().await.can_redo());
|
||||
}
|
||||
EditorCommand::Undo { ret } => {
|
||||
let mut write_guard = self.document.write().await;
|
||||
let UndoResult { operations } = write_guard.undo()?;
|
||||
let md5 = write_guard.md5();
|
||||
let _ = self.save_local_operations(operations, md5).await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
EditorCommand::Redo { ret } => {
|
||||
let mut write_guard = self.document.write().await;
|
||||
let UndoResult { operations } = write_guard.redo()?;
|
||||
let md5 = write_guard.md5();
|
||||
let _ = self.save_local_operations(operations, md5).await?;
|
||||
let _ = ret.send(Ok(()));
|
||||
}
|
||||
EditorCommand::StringifyOperations { ret } => {
|
||||
let data = self.document.read().await.get_operations_json();
|
||||
let _ = ret.send(Ok(data));
|
||||
}
|
||||
EditorCommand::ReadOperations { ret } => {
|
||||
let operations = self.document.read().await.get_operations().clone();
|
||||
let _ = ret.send(Ok(operations));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn save_local_operations(&self, operations: TextOperations, 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()?;
|
||||
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 type TextTransformOperations = TransformOperations<DocumentResolveOperations>;
|
||||
|
||||
pub(crate) type Ret<T> = oneshot::Sender<Result<T, CollaborateError>>;
|
||||
|
||||
pub(crate) enum EditorCommand {
|
||||
ComposeLocalOperations {
|
||||
operations: TextOperations,
|
||||
ret: Ret<()>,
|
||||
},
|
||||
ComposeRemoteOperation {
|
||||
client_operations: TextOperations,
|
||||
ret: Ret<OperationsMD5>,
|
||||
},
|
||||
ResetOperations {
|
||||
operations: TextOperations,
|
||||
ret: Ret<OperationsMD5>,
|
||||
},
|
||||
TransformOperations {
|
||||
operations: TextOperations,
|
||||
ret: Ret<TextTransformOperations>,
|
||||
},
|
||||
Insert {
|
||||
index: usize,
|
||||
data: String,
|
||||
ret: Ret<()>,
|
||||
},
|
||||
Delete {
|
||||
interval: Interval,
|
||||
ret: Ret<()>,
|
||||
},
|
||||
Format {
|
||||
interval: Interval,
|
||||
attribute: AttributeEntry,
|
||||
ret: Ret<()>,
|
||||
},
|
||||
Replace {
|
||||
interval: Interval,
|
||||
data: String,
|
||||
ret: Ret<()>,
|
||||
},
|
||||
CanUndo {
|
||||
ret: oneshot::Sender<bool>,
|
||||
},
|
||||
CanRedo {
|
||||
ret: oneshot::Sender<bool>,
|
||||
},
|
||||
Undo {
|
||||
ret: Ret<()>,
|
||||
},
|
||||
Redo {
|
||||
ret: Ret<()>,
|
||||
},
|
||||
StringifyOperations {
|
||||
ret: Ret<String>,
|
||||
},
|
||||
#[allow(dead_code)]
|
||||
ReadOperations {
|
||||
ret: Ret<TextOperations>,
|
||||
},
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for EditorCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
EditorCommand::ComposeLocalOperations { .. } => "ComposeLocalOperations",
|
||||
EditorCommand::ComposeRemoteOperation { .. } => "ComposeRemoteOperation",
|
||||
EditorCommand::ResetOperations { .. } => "ResetOperations",
|
||||
EditorCommand::TransformOperations { .. } => "TransformOperations",
|
||||
EditorCommand::Insert { .. } => "Insert",
|
||||
EditorCommand::Delete { .. } => "Delete",
|
||||
EditorCommand::Format { .. } => "Format",
|
||||
EditorCommand::Replace { .. } => "Replace",
|
||||
EditorCommand::CanUndo { .. } => "CanUndo",
|
||||
EditorCommand::CanRedo { .. } => "CanRedo",
|
||||
EditorCommand::Undo { .. } => "Undo",
|
||||
EditorCommand::Redo { .. } => "Redo",
|
||||
EditorCommand::StringifyOperations { .. } => "StringifyOperations",
|
||||
EditorCommand::ReadOperations { .. } => "ReadOperations",
|
||||
};
|
||||
f.write_str(s)
|
||||
}
|
||||
}
|
192
frontend/rust-lib/flowy-document/src/web_socket.rs
Normal file
192
frontend/rust-lib/flowy-document/src/web_socket.rs
Normal file
@ -0,0 +1,192 @@
|
||||
use crate::queue::TextTransformOperations;
|
||||
use crate::{queue::EditorCommand, TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS};
|
||||
use bytes::Bytes;
|
||||
use flowy_error::{internal_error, FlowyError, FlowyResult};
|
||||
use flowy_revision::*;
|
||||
use flowy_sync::entities::revision::Revision;
|
||||
use flowy_sync::{
|
||||
entities::{
|
||||
revision::RevisionRange,
|
||||
ws_data::{ClientRevisionWSData, NewDocumentUser, ServerRevisionWSDataType},
|
||||
},
|
||||
errors::CollaborateResult,
|
||||
};
|
||||
use lib_infra::future::{BoxResultFuture, FutureResult};
|
||||
|
||||
use flowy_sync::util::make_operations_from_revisions;
|
||||
use lib_ot::text_delta::TextOperations;
|
||||
use lib_ws::WSConnectState;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tokio::sync::{
|
||||
broadcast,
|
||||
mpsc::{Receiver, Sender},
|
||||
oneshot,
|
||||
};
|
||||
|
||||
pub(crate) type EditorCommandSender = Sender<EditorCommand>;
|
||||
pub(crate) type EditorCommandReceiver = Receiver<EditorCommand>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DocumentResolveOperations(pub TextOperations);
|
||||
|
||||
impl OperationsDeserializer<DocumentResolveOperations> for DocumentResolveOperations {
|
||||
fn deserialize_revisions(revisions: Vec<Revision>) -> FlowyResult<DocumentResolveOperations> {
|
||||
Ok(DocumentResolveOperations(make_operations_from_revisions(revisions)?))
|
||||
}
|
||||
}
|
||||
|
||||
impl OperationsSerializer for DocumentResolveOperations {
|
||||
fn serialize_operations(&self) -> Bytes {
|
||||
self.0.json_bytes()
|
||||
}
|
||||
}
|
||||
|
||||
impl DocumentResolveOperations {
|
||||
pub fn into_inner(self) -> TextOperations {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub type DocumentConflictController = ConflictController<DocumentResolveOperations>;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn make_document_ws_manager(
|
||||
doc_id: String,
|
||||
user_id: String,
|
||||
edit_cmd_tx: EditorCommandSender,
|
||||
rev_manager: Arc<RevisionManager>,
|
||||
rev_web_socket: Arc<dyn RevisionWebSocket>,
|
||||
) -> Arc<RevisionWebSocketManager> {
|
||||
let ws_data_provider = Arc::new(WSDataProvider::new(&doc_id, Arc::new(rev_manager.clone())));
|
||||
let resolver = Arc::new(DocumentConflictResolver { edit_cmd_tx });
|
||||
let conflict_controller =
|
||||
DocumentConflictController::new(&user_id, resolver, Arc::new(ws_data_provider.clone()), rev_manager);
|
||||
let ws_data_stream = Arc::new(DocumentRevisionWSDataStream::new(conflict_controller));
|
||||
let ws_data_sink = Arc::new(DocumentWSDataSink(ws_data_provider));
|
||||
let ping_duration = Duration::from_millis(TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS);
|
||||
let ws_manager = Arc::new(RevisionWebSocketManager::new(
|
||||
"Block",
|
||||
&doc_id,
|
||||
rev_web_socket,
|
||||
ws_data_sink,
|
||||
ws_data_stream,
|
||||
ping_duration,
|
||||
));
|
||||
listen_document_ws_state(&user_id, &doc_id, ws_manager.scribe_state());
|
||||
ws_manager
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn listen_document_ws_state(_user_id: &str, _doc_id: &str, mut subscriber: broadcast::Receiver<WSConnectState>) {
|
||||
tokio::spawn(async move {
|
||||
while let Ok(state) = subscriber.recv().await {
|
||||
match state {
|
||||
WSConnectState::Init => {}
|
||||
WSConnectState::Connecting => {}
|
||||
WSConnectState::Connected => {}
|
||||
WSConnectState::Disconnected => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub(crate) struct DocumentRevisionWSDataStream {
|
||||
conflict_controller: Arc<DocumentConflictController>,
|
||||
}
|
||||
|
||||
impl DocumentRevisionWSDataStream {
|
||||
#[allow(dead_code)]
|
||||
pub fn new(conflict_controller: DocumentConflictController) -> Self {
|
||||
Self {
|
||||
conflict_controller: Arc::new(conflict_controller),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RevisionWSDataStream for DocumentRevisionWSDataStream {
|
||||
fn receive_push_revision(&self, bytes: Bytes) -> BoxResultFuture<(), FlowyError> {
|
||||
let resolver = self.conflict_controller.clone();
|
||||
Box::pin(async move { resolver.receive_bytes(bytes).await })
|
||||
}
|
||||
|
||||
fn receive_ack(&self, id: String, ty: ServerRevisionWSDataType) -> BoxResultFuture<(), FlowyError> {
|
||||
let resolver = self.conflict_controller.clone();
|
||||
Box::pin(async move { resolver.ack_revision(id, ty).await })
|
||||
}
|
||||
|
||||
fn receive_new_user_connect(&self, _new_user: NewDocumentUser) -> BoxResultFuture<(), FlowyError> {
|
||||
// Do nothing by now, just a placeholder for future extension.
|
||||
Box::pin(async move { Ok(()) })
|
||||
}
|
||||
|
||||
fn pull_revisions_in_range(&self, range: RevisionRange) -> BoxResultFuture<(), FlowyError> {
|
||||
let resolver = self.conflict_controller.clone();
|
||||
Box::pin(async move { resolver.send_revisions(range).await })
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct DocumentWSDataSink(pub(crate) Arc<WSDataProvider>);
|
||||
impl RevisionWebSocketSink for DocumentWSDataSink {
|
||||
fn next(&self) -> FutureResult<Option<ClientRevisionWSData>, FlowyError> {
|
||||
let sink_provider = self.0.clone();
|
||||
FutureResult::new(async move { sink_provider.next().await })
|
||||
}
|
||||
}
|
||||
|
||||
struct DocumentConflictResolver {
|
||||
edit_cmd_tx: EditorCommandSender,
|
||||
}
|
||||
|
||||
impl ConflictResolver<DocumentResolveOperations> for DocumentConflictResolver {
|
||||
fn compose_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||
let tx = self.edit_cmd_tx.clone();
|
||||
let operations = operations.into_inner();
|
||||
Box::pin(async move {
|
||||
let (ret, rx) = oneshot::channel();
|
||||
tx.send(EditorCommand::ComposeRemoteOperation {
|
||||
client_operations: operations,
|
||||
ret,
|
||||
})
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
let md5 = rx
|
||||
.await
|
||||
.map_err(|e| FlowyError::internal().context(format!("Compose operations failed: {}", e)))??;
|
||||
Ok(md5)
|
||||
})
|
||||
}
|
||||
|
||||
fn transform_operations(
|
||||
&self,
|
||||
operations: DocumentResolveOperations,
|
||||
) -> BoxResultFuture<TransformOperations<DocumentResolveOperations>, FlowyError> {
|
||||
let tx = self.edit_cmd_tx.clone();
|
||||
let operations = operations.into_inner();
|
||||
Box::pin(async move {
|
||||
let (ret, rx) = oneshot::channel::<CollaborateResult<TextTransformOperations>>();
|
||||
tx.send(EditorCommand::TransformOperations { operations, ret })
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
let transformed_operations = rx
|
||||
.await
|
||||
.map_err(|e| FlowyError::internal().context(format!("Transform operations failed: {}", e)))??;
|
||||
Ok(transformed_operations)
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_operations(&self, operations: DocumentResolveOperations) -> BoxResultFuture<OperationsMD5, FlowyError> {
|
||||
let tx = self.edit_cmd_tx.clone();
|
||||
let operations = operations.into_inner();
|
||||
Box::pin(async move {
|
||||
let (ret, rx) = oneshot::channel();
|
||||
let _ = tx
|
||||
.send(EditorCommand::ResetOperations { operations, ret })
|
||||
.await
|
||||
.map_err(internal_error)?;
|
||||
let md5 = rx
|
||||
.await
|
||||
.map_err(|e| FlowyError::internal().context(format!("Reset operations failed: {}", e)))??;
|
||||
Ok(md5)
|
||||
})
|
||||
}
|
||||
}
|
2
frontend/rust-lib/flowy-document/tests/document/mod.rs
Normal file
2
frontend/rust-lib/flowy-document/tests/document/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod script;
|
||||
mod text_block_test;
|
89
frontend/rust-lib/flowy-document/tests/document/script.rs
Normal file
89
frontend/rust-lib/flowy-document/tests/document/script.rs
Normal file
@ -0,0 +1,89 @@
|
||||
use flowy_document::editor::DocumentEditor;
|
||||
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 std::sync::Arc;
|
||||
use tokio::time::{sleep, Duration};
|
||||
|
||||
pub enum EditorScript {
|
||||
InsertText(&'static str, usize),
|
||||
Delete(Interval),
|
||||
Replace(Interval, &'static str),
|
||||
|
||||
AssertRevisionState(i64, RevisionState),
|
||||
AssertNextSyncRevId(Option<i64>),
|
||||
AssertCurrentRevId(i64),
|
||||
AssertJson(&'static str),
|
||||
}
|
||||
|
||||
pub struct DocumentEditorTest {
|
||||
pub sdk: FlowySDKTest,
|
||||
pub editor: Arc<DocumentEditor>,
|
||||
}
|
||||
|
||||
impl DocumentEditorTest {
|
||||
pub async fn new() -> Self {
|
||||
let sdk = FlowySDKTest::default();
|
||||
let _ = sdk.init_user().await;
|
||||
let test = ViewTest::new_text_block_view(&sdk).await;
|
||||
let editor = sdk
|
||||
.text_block_manager
|
||||
.open_document_editor(&test.view.id)
|
||||
.await
|
||||
.unwrap();
|
||||
Self { sdk, editor }
|
||||
}
|
||||
|
||||
pub async fn run_scripts(mut self, scripts: Vec<EditorScript>) {
|
||||
for script in scripts {
|
||||
self.run_script(script).await;
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_script(&mut self, script: EditorScript) {
|
||||
let rev_manager = self.editor.rev_manager();
|
||||
let cache = rev_manager.revision_cache().await;
|
||||
let _user_id = self.sdk.user_session.user_id().unwrap();
|
||||
|
||||
match script {
|
||||
EditorScript::InsertText(s, offset) => {
|
||||
self.editor.insert(offset, s).await.unwrap();
|
||||
}
|
||||
EditorScript::Delete(interval) => {
|
||||
self.editor.delete(interval).await.unwrap();
|
||||
}
|
||||
EditorScript::Replace(interval, s) => {
|
||||
self.editor.replace(interval, s).await.unwrap();
|
||||
}
|
||||
EditorScript::AssertRevisionState(rev_id, state) => {
|
||||
let record = cache.get(rev_id).await.unwrap();
|
||||
assert_eq!(record.state, state);
|
||||
}
|
||||
EditorScript::AssertCurrentRevId(rev_id) => {
|
||||
assert_eq!(self.editor.rev_manager().rev_id(), rev_id);
|
||||
}
|
||||
EditorScript::AssertNextSyncRevId(rev_id) => {
|
||||
let next_revision = rev_manager.next_sync_revision().await.unwrap();
|
||||
if rev_id.is_none() {
|
||||
assert!(next_revision.is_none(), "Next revision should be None");
|
||||
return;
|
||||
}
|
||||
let next_revision = next_revision.unwrap();
|
||||
let mut notify = rev_manager.ack_notify();
|
||||
let _ = notify.recv().await;
|
||||
assert_eq!(next_revision.rev_id, rev_id.unwrap());
|
||||
}
|
||||
EditorScript::AssertJson(expected) => {
|
||||
let expected_delta: TextOperations = serde_json::from_str(expected).unwrap();
|
||||
let delta = self.editor.document_operations().await.unwrap();
|
||||
if expected_delta != delta {
|
||||
eprintln!("✅ expect: {}", expected,);
|
||||
eprintln!("❌ receive: {}", delta.json_str());
|
||||
}
|
||||
assert_eq!(expected_delta, delta);
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(TEXT_BLOCK_SYNC_INTERVAL_IN_MILLIS)).await;
|
||||
}
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
use crate::document::script::{EditorScript::*, *};
|
||||
use flowy_revision::disk::RevisionState;
|
||||
use lib_ot::core::{count_utf16_code_units, Interval};
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_current_rev_id_check() {
|
||||
let scripts = vec![
|
||||
InsertText("1", 0),
|
||||
AssertCurrentRevId(1),
|
||||
InsertText("2", 1),
|
||||
AssertCurrentRevId(2),
|
||||
InsertText("3", 2),
|
||||
AssertCurrentRevId(3),
|
||||
AssertNextSyncRevId(None),
|
||||
AssertJson(r#"[{"insert":"123\n"}]"#),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_state_check() {
|
||||
let scripts = vec![
|
||||
InsertText("1", 0),
|
||||
InsertText("2", 1),
|
||||
InsertText("3", 2),
|
||||
AssertRevisionState(1, RevisionState::Ack),
|
||||
AssertRevisionState(2, RevisionState::Ack),
|
||||
AssertRevisionState(3, RevisionState::Ack),
|
||||
AssertJson(r#"[{"insert":"123\n"}]"#),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_insert_test() {
|
||||
let scripts = vec![
|
||||
InsertText("1", 0),
|
||||
InsertText("2", 1),
|
||||
InsertText("3", 2),
|
||||
AssertJson(r#"[{"insert":"123\n"}]"#),
|
||||
AssertNextSyncRevId(None),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_insert_in_chinese() {
|
||||
let s = "好".to_owned();
|
||||
let offset = count_utf16_code_units(&s);
|
||||
let scripts = vec![
|
||||
InsertText("你", 0),
|
||||
InsertText("好", offset),
|
||||
AssertJson(r#"[{"insert":"你好\n"}]"#),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_insert_with_emoji() {
|
||||
let s = "😁".to_owned();
|
||||
let offset = count_utf16_code_units(&s);
|
||||
let scripts = vec![
|
||||
InsertText("😁", 0),
|
||||
InsertText("☺️", offset),
|
||||
AssertJson(r#"[{"insert":"😁☺️\n"}]"#),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_delete_in_english() {
|
||||
let scripts = vec![
|
||||
InsertText("1", 0),
|
||||
InsertText("2", 1),
|
||||
InsertText("3", 2),
|
||||
Delete(Interval::new(0, 2)),
|
||||
AssertJson(r#"[{"insert":"3\n"}]"#),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_delete_in_chinese() {
|
||||
let s = "好".to_owned();
|
||||
let offset = count_utf16_code_units(&s);
|
||||
let scripts = vec![
|
||||
InsertText("你", 0),
|
||||
InsertText("好", offset),
|
||||
Delete(Interval::new(0, offset)),
|
||||
AssertJson(r#"[{"insert":"好\n"}]"#),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn text_block_sync_replace_test() {
|
||||
let scripts = vec![
|
||||
InsertText("1", 0),
|
||||
InsertText("2", 1),
|
||||
InsertText("3", 2),
|
||||
Replace(Interval::new(0, 3), "abc"),
|
||||
AssertJson(r#"[{"insert":"abc\n"}]"#),
|
||||
];
|
||||
DocumentEditorTest::new().await.run_scripts(scripts).await;
|
||||
}
|
800
frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs
Normal file
800
frontend/rust-lib/flowy-document/tests/editor/attribute_test.rs
Normal file
@ -0,0 +1,800 @@
|
||||
#![cfg_attr(rustfmt, rustfmt::skip)]
|
||||
use crate::editor::{TestBuilder, TestOp::*};
|
||||
use flowy_sync::client_document::{NewlineDoc, EmptyDoc};
|
||||
use lib_ot::core::{Interval, OperationTransform, NEW_LINE, WHITESPACE, OTString};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
use lib_ot::text_delta::TextOperations;
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Bold(0, Interval::new(3, 5), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"123"},
|
||||
{"insert":"45","attributes":{"bold":true}},
|
||||
{"insert":"6"}
|
||||
]"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_and_invert_all() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
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"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_and_invert_partial_suffix() {
|
||||
let ops = vec![
|
||||
Insert(0, "1234", 0),
|
||||
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"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_and_invert_partial_suffix2() {
|
||||
let ops = vec![
|
||||
Insert(0, "1234", 0),
|
||||
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"}]"#),
|
||||
Bold(0, Interval::new(2, 4), true),
|
||||
AssertDocJson(0, r#"[{"insert":"1234","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_with_new_line() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Bold(0, Interval::new(0, 6), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456","attributes":{"bold":true}},{"insert":"\n"}]"#,
|
||||
),
|
||||
Insert(0, "\n", 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123","attributes":{"bold":true}},{"insert":"\n"},{"insert":"456","attributes":{"bold":true}},{"insert":"\n"}]"#,
|
||||
),
|
||||
Insert(0, "\n", 4),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123","attributes":{"bold":true}},{"insert":"\n\n"},{"insert":"456","attributes":{"bold":true}},{"insert":"\n"}]"#,
|
||||
),
|
||||
Insert(0, "a", 4),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123","attributes":{"bold":true}},{"insert":"\na\n"},{"insert":"456","attributes":{"bold":true}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_and_invert_partial_prefix() {
|
||||
let ops = vec![
|
||||
Insert(0, "1234", 0),
|
||||
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}}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_consecutive() {
|
||||
let ops = vec![
|
||||
Insert(0, "1234", 0),
|
||||
Bold(0, Interval::new(0, 1), true),
|
||||
AssertDocJson(0, r#"[{"insert":"1","attributes":{"bold":true}},{"insert":"234"}]"#),
|
||||
Bold(0, Interval::new(1, 2), true),
|
||||
AssertDocJson(0, r#"[{"insert":"12","attributes":{"bold":true}},{"insert":"34"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_italic() {
|
||||
let ops = vec![
|
||||
Insert(0, "1234", 0),
|
||||
Bold(0, Interval::new(0, 4), true),
|
||||
Italic(0, Interval::new(0, 4), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1234","attributes":{"italic":true,"bold":true}},{"insert":"\n"}]"#,
|
||||
),
|
||||
Insert(0, "5678", 4),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"12345678","attributes":{"bold":true,"italic":true}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_italic2() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Bold(0, Interval::new(0, 6), true),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Italic(0, Interval::new(0, 2), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"italic":true,"bold":true}},
|
||||
{"insert":"3456","attributes":{"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
Italic(0, Interval::new(4, 6), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"italic":true,"bold":true}},
|
||||
{"insert":"34","attributes":{"bold":true}},
|
||||
{"insert":"56","attributes":{"italic":true,"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_italic3() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456789", 0),
|
||||
Bold(0, Interval::new(0, 5), true),
|
||||
Italic(0, Interval::new(0, 2), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"345","attributes":{"bold":true}},{"insert":"6789"}]
|
||||
"#,
|
||||
),
|
||||
Italic(0, Interval::new(2, 4), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"1234","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"5","attributes":{"bold":true}},
|
||||
{"insert":"6789"}]
|
||||
"#,
|
||||
),
|
||||
Bold(0, Interval::new(7, 9), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"1234","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"5","attributes":{"bold":true}},
|
||||
{"insert":"67"},
|
||||
{"insert":"89","attributes":{"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bold_added_italic_delete() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456789", 0),
|
||||
Bold(0, Interval::new(0, 5), true),
|
||||
Italic(0, Interval::new(0, 2), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"italic":true,"bold":true}},
|
||||
{"insert":"345","attributes":{"bold":true}},{"insert":"6789"}]
|
||||
"#,
|
||||
),
|
||||
Italic(0, Interval::new(2, 4), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"1234","attributes":{"bold":true,"italic":true}}
|
||||
,{"insert":"5","attributes":{"bold":true}},{"insert":"6789"}]"#,
|
||||
),
|
||||
Bold(0, Interval::new(7, 9), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"1234","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"5","attributes":{"bold":true}},{"insert":"67"},
|
||||
{"insert":"89","attributes":{"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
Delete(0, Interval::new(0, 5)),
|
||||
AssertDocJson(0, r#"[{"insert":"67"},{"insert":"89","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_merge_inserted_text_with_same_attribute() {
|
||||
let ops = vec![
|
||||
InsertBold(0, "123", Interval::new(0, 3)),
|
||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#),
|
||||
InsertBold(0, "456", Interval::new(3, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_compose_attr_attributes_with_attr_attributes_test() {
|
||||
let ops = vec![
|
||||
InsertBold(0, "123456", Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
InsertBold(1, "7", Interval::new(0, 1)),
|
||||
AssertDocJson(1, r#"[{"insert":"7","attributes":{"bold":true}}]"#),
|
||||
Transform(0, 1),
|
||||
AssertDocJson(0, r#"[{"insert":"1234567","attributes":{"bold":true}}]"#),
|
||||
AssertDocJson(1, r#"[{"insert":"1234567","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_compose_attr_attributes_with_attr_attributes_test2() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Bold(0, Interval::new(0, 6), true),
|
||||
Italic(0, Interval::new(0, 2), true),
|
||||
Italic(0, Interval::new(4, 6), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"34","attributes":{"bold":true}},
|
||||
{"insert":"56","attributes":{"italic":true,"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
InsertBold(1, "7", Interval::new(0, 1)),
|
||||
AssertDocJson(1, r#"[{"insert":"7","attributes":{"bold":true}}]"#),
|
||||
Transform(0, 1),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"italic":true,"bold":true}},
|
||||
{"insert":"34","attributes":{"bold":true}},
|
||||
{"insert":"56","attributes":{"italic":true,"bold":true}},
|
||||
{"insert":"7","attributes":{"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
AssertDocJson(
|
||||
1,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"italic":true,"bold":true}},
|
||||
{"insert":"34","attributes":{"bold":true}},
|
||||
{"insert":"56","attributes":{"italic":true,"bold":true}},
|
||||
{"insert":"7","attributes":{"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_compose_attr_attributes_with_no_attr_attributes_test() {
|
||||
let expected = r#"[{"insert":"123456","attributes":{"bold":true}},{"insert":"7"}]"#;
|
||||
|
||||
let ops = vec![
|
||||
InsertBold(0, "123456", Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Insert(1, "7", 0),
|
||||
AssertDocJson(1, r#"[{"insert":"7"}]"#),
|
||||
Transform(0, 1),
|
||||
AssertDocJson(0, expected),
|
||||
AssertDocJson(1, expected),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_replace_heading() {
|
||||
let ops = vec![
|
||||
InsertBold(0, "123456", Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Delete(0, Interval::new(0, 2)),
|
||||
AssertDocJson(0, r#"[{"insert":"3456","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_replace_trailing() {
|
||||
let ops = vec![
|
||||
InsertBold(0, "123456", Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Delete(0, Interval::new(5, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"12345","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_replace_middle() {
|
||||
let ops = vec![
|
||||
InsertBold(0, "123456", Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Delete(0, Interval::new(0, 2)),
|
||||
AssertDocJson(0, r#"[{"insert":"3456","attributes":{"bold":true}}]"#),
|
||||
Delete(0, Interval::new(2, 4)),
|
||||
AssertDocJson(0, r#"[{"insert":"34","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_replace_all() {
|
||||
let ops = vec![
|
||||
InsertBold(0, "123456", Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Delete(0, Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_replace_with_text() {
|
||||
let ops = vec![
|
||||
InsertBold(0, "123456", Interval::new(0, 6)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Replace(0, Interval::new(0, 3), "ab"),
|
||||
AssertDocJson(0, r#"[{"insert":"ab"},{"insert":"456","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_header_insert_newline_at_middle() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Header(0, Interval::new(0, 6), 1),
|
||||
AssertDocJson(0, r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":1}}]"#),
|
||||
Insert(0, "\n", 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n","attributes":{"header":1}},{"insert":"456"},{"insert":"\n","attributes":{"header":1}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_header_insert_double_newline_at_middle() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Header(0, Interval::new(0, 6), 1),
|
||||
Insert(0, "\n", 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n","attributes":{"header":1}},{"insert":"456"},{"insert":"\n","attributes":{"header":1}}]"#,
|
||||
),
|
||||
Insert(0, "\n", 4),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n\n","attributes":{"header":1}},{"insert":"456"},{"insert":"\n","attributes":{"header":1}}]"#,
|
||||
),
|
||||
Insert(0, "\n", 4),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n\n","attributes":{"header":1}},{"insert":"\n456"},{"insert":"\n","attributes":{"header":1}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_header_insert_newline_at_trailing() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Header(0, Interval::new(0, 6), 1),
|
||||
Insert(0, "\n", 6),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":1}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_header_insert_double_newline_at_trailing() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Header(0, Interval::new(0, 6), 1),
|
||||
Insert(0, "\n", 6),
|
||||
Insert(0, "\n", 7),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":1}},{"insert":"\n\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_added() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Link(0, Interval::new(0, 6), "https://appflowy.io"),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_format_with_bold() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Link(0, Interval::new(0, 6), "https://appflowy.io"),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"123","attributes":{"bold":true,"link":"https://appflowy.io"}},
|
||||
{"insert":"456","attributes":{"link":"https://appflowy.io"}},
|
||||
{"insert":"\n"}]
|
||||
"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_insert_char_at_head() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Link(0, Interval::new(0, 6), "https://appflowy.io"),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
Insert(0, "a", 0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"a"},{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_insert_char_at_middle() {
|
||||
let ops = vec![
|
||||
Insert(0, "1256", 0),
|
||||
Link(0, Interval::new(0, 4), "https://appflowy.io"),
|
||||
Insert(0, "34", 2),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_insert_char_at_trailing() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Link(0, Interval::new(0, 6), "https://appflowy.io"),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
Insert(0, "a", 6),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"a\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_insert_newline_at_middle() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Link(0, Interval::new(0, 6), "https://appflowy.io"),
|
||||
Insert(0, NEW_LINE, 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"},{"insert":"456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_auto_format() {
|
||||
let site = "https://appflowy.io";
|
||||
let ops = vec![
|
||||
Insert(0, site, 0),
|
||||
AssertDocJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
|
||||
Insert(0, WHITESPACE, site.len()),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"https://appflowy.io","attributes":{"link":"https://appflowy.io/"}},{"insert":" \n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_auto_format_exist() {
|
||||
let site = "https://appflowy.io";
|
||||
let ops = vec![
|
||||
Insert(0, site, 0),
|
||||
Link(0, Interval::new(0, site.len()), site),
|
||||
Insert(0, WHITESPACE, site.len()),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"https://appflowy.io","attributes":{"link":"https://appflowy.io/"}},{"insert":" \n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_link_auto_format_exist2() {
|
||||
let site = "https://appflowy.io";
|
||||
let ops = vec![
|
||||
Insert(0, site, 0),
|
||||
Link(0, Interval::new(0, site.len() / 2), site),
|
||||
Insert(0, WHITESPACE, site.len()),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"https://a","attributes":{"link":"https://appflowy.io"}},{"insert":"ppflowy.io \n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bullet_added() {
|
||||
let ops = vec![
|
||||
Insert(0, "12", 0),
|
||||
Bullet(0, Interval::new(0, 1), true),
|
||||
AssertDocJson(0, r#"[{"insert":"12"},{"insert":"\n","attributes":{"list":"bullet"}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bullet_added_2() {
|
||||
let ops = vec![
|
||||
Insert(0, "1", 0),
|
||||
Bullet(0, Interval::new(0, 1), true),
|
||||
AssertDocJson(0, r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}}]"#),
|
||||
Insert(0, NEW_LINE, 1),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Insert(0, "2", 2),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"2"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bullet_remove_partial() {
|
||||
let ops = vec![
|
||||
Insert(0, "1", 0),
|
||||
Bullet(0, Interval::new(0, 1), true),
|
||||
Insert(0, NEW_LINE, 1),
|
||||
Insert(0, "2", 2),
|
||||
Bullet(0, Interval::new(2, 3), false),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"2\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_bullet_auto_exit() {
|
||||
let ops = vec![
|
||||
Insert(0, "1", 0),
|
||||
Bullet(0, Interval::new(0, 1), true),
|
||||
Insert(0, NEW_LINE, 1),
|
||||
Insert(0, NEW_LINE, 2),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_preserve_block_when_insert_newline_inside() {
|
||||
let ops = vec![
|
||||
Insert(0, "12", 0),
|
||||
Bullet(0, Interval::new(0, 2), true),
|
||||
Insert(0, NEW_LINE, 2),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"12"},{"insert":"\n\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Insert(0, "34", 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12"},{"insert":"\n","attributes":{"list":"bullet"}},
|
||||
{"insert":"34"},{"insert":"\n","attributes":{"list":"bullet"}}
|
||||
]"#,
|
||||
),
|
||||
Insert(0, NEW_LINE, 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12"},{"insert":"\n\n","attributes":{"list":"bullet"}},
|
||||
{"insert":"34"},{"insert":"\n","attributes":{"list":"bullet"}}
|
||||
]"#,
|
||||
),
|
||||
Insert(0, "ab", 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12"},{"insert":"\n","attributes":{"list":"bullet"}},
|
||||
{"insert":"ab"},{"insert":"\n","attributes":{"list":"bullet"}},
|
||||
{"insert":"34"},{"insert":"\n","attributes":{"list":"bullet"}}
|
||||
]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_preserve_header_format_on_merge() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Header(0, Interval::new(0, 6), 1),
|
||||
Insert(0, NEW_LINE, 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n","attributes":{"header":1}},{"insert":"456"},{"insert":"\n","attributes":{"header":1}}]"#,
|
||||
),
|
||||
Delete(0, Interval::new(3, 4)),
|
||||
AssertDocJson(0, r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":1}}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_format_emoji() {
|
||||
let emoji_s = "👋 ";
|
||||
let s: OTString = emoji_s.into();
|
||||
let len = s.utf16_len();
|
||||
assert_eq!(3, len);
|
||||
assert_eq!(2, s.graphemes(true).count());
|
||||
|
||||
let ops = vec![
|
||||
Insert(0, emoji_s, 0),
|
||||
AssertDocJson(0, r#"[{"insert":"👋 \n"}]"#),
|
||||
Header(0, Interval::new(0, len), 1),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"👋 "},{"insert":"\n","attributes":{"header":1}}]"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_preserve_list_format_on_merge() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Bullet(0, Interval::new(0, 6), true),
|
||||
Insert(0, NEW_LINE, 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"456"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Delete(0, Interval::new(3, 4)),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_compose() {
|
||||
let mut delta = TextOperations::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(),
|
||||
];
|
||||
|
||||
for d in deltas {
|
||||
delta = delta.compose(&d).unwrap();
|
||||
}
|
||||
assert_eq!(
|
||||
delta.json_str(),
|
||||
r#"[{"insert":"a"},{"insert":"\n","attributes":{"list":"unchecked"}},{"insert":"\n"}]"#
|
||||
);
|
||||
|
||||
let ops = vec![
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
Insert(0, "a", 0),
|
||||
AssertDocJson(0, r#"[{"insert":"a\n"}]"#),
|
||||
Bullet(0, Interval::new(0, 1), true),
|
||||
AssertDocJson(0, r#"[{"insert":"a"},{"insert":"\n","attributes":{"list":"bullet"}}]"#),
|
||||
Insert(0, NEW_LINE, 1),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"a"},{"insert":"\n\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Insert(0, NEW_LINE, 2),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"a"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
330
frontend/rust-lib/flowy-document/tests/editor/mod.rs
Normal file
330
frontend/rust-lib/flowy-document/tests/editor/mod.rs
Normal file
@ -0,0 +1,330 @@
|
||||
#![allow(clippy::module_inception)]
|
||||
mod attribute_test;
|
||||
mod op_test;
|
||||
mod serde_test;
|
||||
mod undo_redo_test;
|
||||
|
||||
use derive_more::Display;
|
||||
use flowy_sync::client_document::{ClientDocument, InitialDocument};
|
||||
use lib_ot::{
|
||||
core::*,
|
||||
text_delta::{BuildInTextAttribute, TextOperations},
|
||||
};
|
||||
use rand::{prelude::*, Rng as WrappedRng};
|
||||
use std::{sync::Once, time::Duration};
|
||||
|
||||
#[derive(Clone, Debug, Display)]
|
||||
pub enum TestOp {
|
||||
#[display(fmt = "Insert")]
|
||||
Insert(usize, &'static str, usize),
|
||||
|
||||
// delta_i, s, start, length,
|
||||
#[display(fmt = "InsertBold")]
|
||||
InsertBold(usize, &'static str, Interval),
|
||||
|
||||
// delta_i, start, length, enable
|
||||
#[display(fmt = "Bold")]
|
||||
Bold(usize, Interval, bool),
|
||||
|
||||
#[display(fmt = "Delete")]
|
||||
Delete(usize, Interval),
|
||||
|
||||
#[display(fmt = "Replace")]
|
||||
Replace(usize, Interval, &'static str),
|
||||
|
||||
#[display(fmt = "Italic")]
|
||||
Italic(usize, Interval, bool),
|
||||
|
||||
#[display(fmt = "Header")]
|
||||
Header(usize, Interval, usize),
|
||||
|
||||
#[display(fmt = "Link")]
|
||||
Link(usize, Interval, &'static str),
|
||||
|
||||
#[display(fmt = "Bullet")]
|
||||
Bullet(usize, Interval, bool),
|
||||
|
||||
#[display(fmt = "Transform")]
|
||||
Transform(usize, usize),
|
||||
|
||||
#[display(fmt = "TransformPrime")]
|
||||
TransformPrime(usize, usize),
|
||||
|
||||
// invert the delta_a base on the delta_b
|
||||
#[display(fmt = "Invert")]
|
||||
Invert(usize, usize),
|
||||
|
||||
#[display(fmt = "Undo")]
|
||||
Undo(usize),
|
||||
|
||||
#[display(fmt = "Redo")]
|
||||
Redo(usize),
|
||||
|
||||
#[display(fmt = "Wait")]
|
||||
Wait(usize),
|
||||
|
||||
#[display(fmt = "AssertStr")]
|
||||
AssertStr(usize, &'static str),
|
||||
|
||||
#[display(fmt = "AssertDocJson")]
|
||||
AssertDocJson(usize, &'static str),
|
||||
|
||||
#[display(fmt = "AssertPrimeJson")]
|
||||
AssertPrimeJson(usize, &'static str),
|
||||
|
||||
#[display(fmt = "DocComposeDelta")]
|
||||
DocComposeDelta(usize, usize),
|
||||
|
||||
#[display(fmt = "ApplyPrimeDelta")]
|
||||
DocComposePrime(usize, usize),
|
||||
}
|
||||
|
||||
pub struct TestBuilder {
|
||||
documents: Vec<ClientDocument>,
|
||||
deltas: Vec<Option<TextOperations>>,
|
||||
primes: Vec<Option<TextOperations>>,
|
||||
}
|
||||
|
||||
impl TestBuilder {
|
||||
pub fn new() -> Self {
|
||||
static INIT: Once = Once::new();
|
||||
INIT.call_once(|| {
|
||||
let _ = color_eyre::install();
|
||||
// let subscriber = FmtSubscriber::builder().with_max_level(Level::INFO).finish();
|
||||
// tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
});
|
||||
|
||||
Self {
|
||||
documents: vec![],
|
||||
deltas: vec![],
|
||||
primes: vec![],
|
||||
}
|
||||
}
|
||||
|
||||
fn run_op(&mut self, op: &TestOp) {
|
||||
tracing::trace!("***************** 😈{} *******************", &op);
|
||||
match op {
|
||||
TestOp::Insert(delta_i, s, index) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let delta = document.insert(*index, s).unwrap();
|
||||
tracing::debug!("Insert delta: {}", delta.json_str());
|
||||
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::Delete(delta_i, iv) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let delta = document.replace(*iv, "").unwrap();
|
||||
tracing::trace!("Delete delta: {}", delta.json_str());
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::Replace(delta_i, iv, s) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let delta = document.replace(*iv, s).unwrap();
|
||||
tracing::trace!("Replace delta: {}", delta.json_str());
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::InsertBold(delta_i, s, iv) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
document.insert(iv.start, s).unwrap();
|
||||
document.format(*iv, BuildInTextAttribute::Bold(true)).unwrap();
|
||||
}
|
||||
TestOp::Bold(delta_i, iv, enable) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let attribute = BuildInTextAttribute::Bold(*enable);
|
||||
let delta = document.format(*iv, attribute).unwrap();
|
||||
tracing::trace!("Bold delta: {}", delta.json_str());
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::Italic(delta_i, iv, enable) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let attribute = match *enable {
|
||||
true => BuildInTextAttribute::Italic(true),
|
||||
false => BuildInTextAttribute::Italic(false),
|
||||
};
|
||||
let delta = document.format(*iv, attribute).unwrap();
|
||||
tracing::trace!("Italic delta: {}", delta.json_str());
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::Header(delta_i, iv, level) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let attribute = BuildInTextAttribute::Header(*level);
|
||||
let delta = document.format(*iv, attribute).unwrap();
|
||||
tracing::trace!("Header delta: {}", delta.json_str());
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::Link(delta_i, iv, link) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let attribute = BuildInTextAttribute::Link(link.to_owned());
|
||||
let delta = document.format(*iv, attribute).unwrap();
|
||||
tracing::trace!("Link delta: {}", delta.json_str());
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::Bullet(delta_i, iv, enable) => {
|
||||
let document = &mut self.documents[*delta_i];
|
||||
let attribute = BuildInTextAttribute::Bullet(*enable);
|
||||
let delta = document.format(*iv, attribute).unwrap();
|
||||
tracing::debug!("Bullet delta: {}", delta.json_str());
|
||||
|
||||
self.deltas.insert(*delta_i, Some(delta));
|
||||
}
|
||||
TestOp::Transform(delta_a_i, delta_b_i) => {
|
||||
let (a_prime, b_prime) = self.documents[*delta_a_i]
|
||||
.get_operations()
|
||||
.transform(self.documents[*delta_b_i].get_operations())
|
||||
.unwrap();
|
||||
tracing::trace!("a:{:?},b:{:?}", a_prime, b_prime);
|
||||
|
||||
let data_left = self.documents[*delta_a_i].get_operations().compose(&b_prime).unwrap();
|
||||
let data_right = self.documents[*delta_b_i].get_operations().compose(&a_prime).unwrap();
|
||||
|
||||
self.documents[*delta_a_i].set_operations(data_left);
|
||||
self.documents[*delta_b_i].set_operations(data_right);
|
||||
}
|
||||
TestOp::TransformPrime(a_doc_index, b_doc_index) => {
|
||||
let (prime_left, prime_right) = self.documents[*a_doc_index]
|
||||
.get_operations()
|
||||
.transform(self.documents[*b_doc_index].get_operations())
|
||||
.unwrap();
|
||||
|
||||
self.primes.insert(*a_doc_index, Some(prime_left));
|
||||
self.primes.insert(*b_doc_index, Some(prime_right));
|
||||
}
|
||||
TestOp::Invert(delta_a_i, delta_b_i) => {
|
||||
let delta_a = &self.documents[*delta_a_i].get_operations();
|
||||
let delta_b = &self.documents[*delta_b_i].get_operations();
|
||||
tracing::debug!("Invert: ");
|
||||
tracing::debug!("a: {}", delta_a.json_str());
|
||||
tracing::debug!("b: {}", delta_b.json_str());
|
||||
|
||||
let (_, b_prime) = delta_a.transform(delta_b).unwrap();
|
||||
let undo = b_prime.invert(delta_a);
|
||||
|
||||
let new_delta = delta_a.compose(&b_prime).unwrap();
|
||||
tracing::debug!("new delta: {}", new_delta.json_str());
|
||||
tracing::debug!("undo delta: {}", undo.json_str());
|
||||
|
||||
let new_delta_after_undo = new_delta.compose(&undo).unwrap();
|
||||
|
||||
tracing::debug!("inverted delta a: {}", new_delta_after_undo.to_string());
|
||||
|
||||
assert_eq!(delta_a, &&new_delta_after_undo);
|
||||
|
||||
self.documents[*delta_a_i].set_operations(new_delta_after_undo);
|
||||
}
|
||||
TestOp::Undo(delta_i) => {
|
||||
self.documents[*delta_i].undo().unwrap();
|
||||
}
|
||||
TestOp::Redo(delta_i) => {
|
||||
self.documents[*delta_i].redo().unwrap();
|
||||
}
|
||||
TestOp::Wait(mills_sec) => {
|
||||
std::thread::sleep(Duration::from_millis(*mills_sec as u64));
|
||||
}
|
||||
TestOp::AssertStr(delta_i, expected) => {
|
||||
assert_eq!(&self.documents[*delta_i].to_content(), expected);
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
if expected_delta != target_delta {
|
||||
log::error!("✅ expect: {}", expected,);
|
||||
log::error!("❌ 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();
|
||||
|
||||
if expected_prime != target_prime {
|
||||
log::error!("✅ expect prime: {}", expected,);
|
||||
log::error!("❌ receive prime: {}", prime_json);
|
||||
}
|
||||
assert_eq!(target_prime, expected_prime);
|
||||
}
|
||||
TestOp::DocComposeDelta(doc_index, delta_i) => {
|
||||
let delta = self.deltas.get(*delta_i).unwrap().as_ref().unwrap();
|
||||
self.documents[*doc_index].compose_operations(delta.clone()).unwrap();
|
||||
}
|
||||
TestOp::DocComposePrime(doc_index, prime_i) => {
|
||||
let delta = self
|
||||
.primes
|
||||
.get(*prime_i)
|
||||
.expect("Must call TransformPrime first")
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let new_delta = self.documents[*doc_index].get_operations().compose(delta).unwrap();
|
||||
self.documents[*doc_index].set_operations(new_delta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_scripts<C: InitialDocument>(mut self, scripts: Vec<TestOp>) {
|
||||
self.documents = vec![ClientDocument::new::<C>(), ClientDocument::new::<C>()];
|
||||
self.primes = vec![None, None];
|
||||
self.deltas = vec![None, None];
|
||||
for (_i, op) in scripts.iter().enumerate() {
|
||||
self.run_op(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Rng(StdRng);
|
||||
|
||||
impl Default for Rng {
|
||||
fn default() -> Self {
|
||||
Rng(StdRng::from_rng(thread_rng()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Rng {
|
||||
#[allow(dead_code)]
|
||||
pub fn from_seed(seed: [u8; 32]) -> Self {
|
||||
Rng(StdRng::from_seed(seed))
|
||||
}
|
||||
|
||||
pub fn gen_string(&mut self, len: usize) -> String {
|
||||
(0..len)
|
||||
.map(|_| {
|
||||
let c = self.0.gen::<char>();
|
||||
format!("{:x}", c as u32)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn gen_delta(&mut self, s: &str) -> TextOperations {
|
||||
let mut delta = TextOperations::default();
|
||||
let s = OTString::from(s);
|
||||
loop {
|
||||
let left = s.utf16_len() - delta.utf16_base_len;
|
||||
if left == 0 {
|
||||
break;
|
||||
}
|
||||
let i = if left == 1 {
|
||||
1
|
||||
} else {
|
||||
1 + self.0.gen_range(0..std::cmp::min(left - 1, 20))
|
||||
};
|
||||
match self.0.gen_range(0.0..1.0) {
|
||||
f if f < 0.2 => {
|
||||
delta.insert(&self.gen_string(i), AttributeHashMap::default());
|
||||
}
|
||||
f if f < 0.4 => {
|
||||
delta.delete(i);
|
||||
}
|
||||
_ => {
|
||||
delta.retain(i, AttributeHashMap::default());
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.0.gen_range(0.0..1.0) < 0.3 {
|
||||
delta.insert(&("1".to_owned() + &self.gen_string(10)), AttributeHashMap::default());
|
||||
}
|
||||
delta
|
||||
}
|
||||
}
|
750
frontend/rust-lib/flowy-document/tests/editor/op_test.rs
Normal file
750
frontend/rust-lib/flowy-document/tests/editor/op_test.rs
Normal file
@ -0,0 +1,750 @@
|
||||
#![allow(clippy::all)]
|
||||
use crate::editor::{Rng, TestBuilder, TestOp::*};
|
||||
use flowy_sync::client_document::{EmptyDoc, NewlineDoc};
|
||||
use lib_ot::text_delta::TextOperationBuilder;
|
||||
use lib_ot::{core::Interval, core::*, text_delta::TextOperations};
|
||||
|
||||
#[test]
|
||||
fn attributes_insert_text() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(0, "456", 3),
|
||||
AssertDocJson(0, r#"[{"insert":"123456"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_insert_text_at_head() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(0, "456", 0),
|
||||
AssertDocJson(0, r#"[{"insert":"456123"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_insert_text_at_middle() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(0, "456", 1),
|
||||
AssertDocJson(0, r#"[{"insert":"145623"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_1() {
|
||||
let operations = OperationsBuilder::new().insert("123").insert("4").build();
|
||||
let delta = TextOperationBuilder::from_operations(operations);
|
||||
|
||||
let mut iterator = OperationIterator::from_interval(&delta, Interval::new(0, 4));
|
||||
assert_eq!(iterator.ops(), delta.ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_2() {
|
||||
let mut delta = TextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("123");
|
||||
let insert_b = DeltaOperation::insert("4");
|
||||
let insert_c = DeltaOperation::insert("5");
|
||||
let retain_a = DeltaOperation::retain(3);
|
||||
|
||||
delta.add(insert_a.clone());
|
||||
delta.add(retain_a.clone());
|
||||
delta.add(insert_b.clone());
|
||||
delta.add(insert_c.clone());
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(0, 2)).ops(),
|
||||
vec![DeltaOperation::insert("12")]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(1, 3)).ops(),
|
||||
vec![DeltaOperation::insert("23")]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(0, 3)).ops(),
|
||||
vec![insert_a.clone()]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(0, 4)).ops(),
|
||||
vec![insert_a.clone(), DeltaOperation::retain(1)]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(0, 6)).ops(),
|
||||
vec![insert_a.clone(), retain_a.clone()]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(0, 7)).ops(),
|
||||
vec![insert_a.clone(), retain_a.clone(), insert_b.clone()]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_3() {
|
||||
let mut delta = TextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("123456");
|
||||
delta.add(insert_a.clone());
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(3, 5)).ops(),
|
||||
vec![DeltaOperation::insert("45")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_4() {
|
||||
let mut delta = TextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12");
|
||||
let insert_b = DeltaOperation::insert("34");
|
||||
let insert_c = DeltaOperation::insert("56");
|
||||
|
||||
delta.ops.push(insert_a.clone());
|
||||
delta.ops.push(insert_b.clone());
|
||||
delta.ops.push(insert_c.clone());
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(0, 2)).ops(),
|
||||
vec![insert_a]
|
||||
);
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(2, 4)).ops(),
|
||||
vec![insert_b]
|
||||
);
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(4, 6)).ops(),
|
||||
vec![insert_c]
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(2, 5)).ops(),
|
||||
vec![DeltaOperation::insert("34"), DeltaOperation::insert("5")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_5() {
|
||||
let mut delta = TextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("123456");
|
||||
let insert_b = DeltaOperation::insert("789");
|
||||
delta.ops.push(insert_a.clone());
|
||||
delta.ops.push(insert_b.clone());
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(4, 8)).ops(),
|
||||
vec![DeltaOperation::insert("56"), DeltaOperation::insert("78")]
|
||||
);
|
||||
|
||||
// assert_eq!(
|
||||
// DeltaIter::from_interval(&delta, Interval::new(8, 9)).ops(),
|
||||
// vec![Builder::insert("9")]
|
||||
// );
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_6() {
|
||||
let mut delta = TextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12345678");
|
||||
delta.add(insert_a.clone());
|
||||
assert_eq!(
|
||||
OperationIterator::from_interval(&delta, Interval::new(4, 6)).ops(),
|
||||
vec![DeltaOperation::insert("56")]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_get_ops_in_interval_7() {
|
||||
let mut delta = TextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12345");
|
||||
let retain_a = DeltaOperation::retain(3);
|
||||
|
||||
delta.add(insert_a.clone());
|
||||
delta.add(retain_a.clone());
|
||||
|
||||
let mut iter_1 = OperationIterator::from_offset(&delta, 2);
|
||||
assert_eq!(iter_1.next_op().unwrap(), DeltaOperation::insert("345"));
|
||||
assert_eq!(iter_1.next_op().unwrap(), DeltaOperation::retain(3));
|
||||
|
||||
let mut iter_2 = OperationIterator::new(&delta);
|
||||
assert_eq!(iter_2.next_op_with_len(2).unwrap(), DeltaOperation::insert("12"));
|
||||
assert_eq!(iter_2.next_op().unwrap(), DeltaOperation::insert("345"));
|
||||
|
||||
assert_eq!(iter_2.next_op().unwrap(), DeltaOperation::retain(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_op_seek() {
|
||||
let mut delta = TextOperations::default();
|
||||
let insert_a = DeltaOperation::insert("12345");
|
||||
let retain_a = DeltaOperation::retain(3);
|
||||
delta.add(insert_a.clone());
|
||||
delta.add(retain_a.clone());
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
iter.seek::<OpMetric>(1);
|
||||
assert_eq!(iter.next_op().unwrap(), retain_a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_utf16_code_unit_seek() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
iter.seek::<Utf16CodeUnitMetric>(3);
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("45"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_utf16_code_unit_seek_with_attributes() {
|
||||
let mut delta = TextOperations::default();
|
||||
let attributes = AttributeBuilder::new()
|
||||
.insert("bold", true)
|
||||
.insert("italic", true)
|
||||
.build();
|
||||
|
||||
delta.add(DeltaOperation::insert_with_attributes("1234", attributes.clone()));
|
||||
delta.add(DeltaOperation::insert("\n"));
|
||||
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
iter.seek::<Utf16CodeUnitMetric>(0);
|
||||
|
||||
assert_eq!(
|
||||
iter.next_op_with_len(4).unwrap(),
|
||||
DeltaOperation::insert_with_attributes("1234", attributes),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("12"));
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("34"));
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("5"));
|
||||
assert_eq!(iter.next_op_with_len(1), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_with_chinese() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("你好"));
|
||||
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 2);
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("你好"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_with_english() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("ab"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 2);
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::insert("ab"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_after_seek() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 5);
|
||||
iter.seek::<Utf16CodeUnitMetric>(3);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 2);
|
||||
assert_eq!(iter.next_op_with_len(1).unwrap(), DeltaOperation::insert("4"));
|
||||
assert_eq!(iter.next_op_len().unwrap(), 1);
|
||||
assert_eq!(iter.next_op().unwrap(), DeltaOperation::insert("5"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_len_none() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
|
||||
assert_eq!(iter.next_op_len().unwrap(), 5);
|
||||
assert_eq!(iter.next_op_with_len(5).unwrap(), DeltaOperation::insert("12345"));
|
||||
assert_eq!(iter.next_op_len(), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_with_len_zero() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
assert_eq!(iter.next_op_with_len(0), None,);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_next_op_with_len_cross_op_return_last() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("12345"));
|
||||
delta.add(DeltaOperation::retain(1));
|
||||
delta.add(DeltaOperation::insert("678"));
|
||||
|
||||
let mut iter = OperationIterator::new(&delta);
|
||||
iter.seek::<Utf16CodeUnitMetric>(4);
|
||||
assert_eq!(iter.next_op_len().unwrap(), 1);
|
||||
assert_eq!(iter.next_op_with_len(2).unwrap(), DeltaOperation::retain(1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lengths() {
|
||||
let mut delta = TextOperations::default();
|
||||
assert_eq!(delta.utf16_base_len, 0);
|
||||
assert_eq!(delta.utf16_target_len, 0);
|
||||
delta.retain(5, AttributeHashMap::default());
|
||||
assert_eq!(delta.utf16_base_len, 5);
|
||||
assert_eq!(delta.utf16_target_len, 5);
|
||||
delta.insert("abc", AttributeHashMap::default());
|
||||
assert_eq!(delta.utf16_base_len, 5);
|
||||
assert_eq!(delta.utf16_target_len, 8);
|
||||
delta.retain(2, AttributeHashMap::default());
|
||||
assert_eq!(delta.utf16_base_len, 7);
|
||||
assert_eq!(delta.utf16_target_len, 10);
|
||||
delta.delete(2);
|
||||
assert_eq!(delta.utf16_base_len, 9);
|
||||
assert_eq!(delta.utf16_target_len, 10);
|
||||
}
|
||||
#[test]
|
||||
fn sequence() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.retain(5, AttributeHashMap::default());
|
||||
delta.retain(0, AttributeHashMap::default());
|
||||
delta.insert("appflowy", AttributeHashMap::default());
|
||||
delta.insert("", AttributeHashMap::default());
|
||||
delta.delete(3);
|
||||
delta.delete(0);
|
||||
assert_eq!(delta.ops.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_1000() {
|
||||
for _ in 0..1 {
|
||||
let mut rng = Rng::default();
|
||||
let s: OTString = rng.gen_string(50).into();
|
||||
let delta = rng.gen_delta(&s);
|
||||
assert_eq!(s.utf16_len(), delta.utf16_base_len);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_test() {
|
||||
let s = "hello";
|
||||
let delta_a = DeltaBuilder::new().insert(s).build();
|
||||
let delta_b = DeltaBuilder::new().retain(s.len()).insert(", AppFlowy").build();
|
||||
|
||||
let after_a = delta_a.content().unwrap();
|
||||
let after_b = delta_b.apply(&after_a).unwrap();
|
||||
assert_eq!("hello, AppFlowy", &after_b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn base_len_test() {
|
||||
let mut delta_a = TextOperations::default();
|
||||
delta_a.insert("a", AttributeHashMap::default());
|
||||
delta_a.insert("b", AttributeHashMap::default());
|
||||
delta_a.insert("c", AttributeHashMap::default());
|
||||
|
||||
let s = "hello world,".to_owned();
|
||||
delta_a.delete(s.len());
|
||||
let after_a = delta_a.apply(&s).unwrap();
|
||||
|
||||
delta_a.insert("d", AttributeHashMap::default());
|
||||
assert_eq!("abc", &after_a);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invert() {
|
||||
for _ in 0..1000 {
|
||||
let mut rng = Rng::default();
|
||||
let s = rng.gen_string(50);
|
||||
let delta_a = rng.gen_delta(&s);
|
||||
let delta_b = delta_a.invert_str(&s);
|
||||
assert_eq!(delta_a.utf16_base_len, delta_b.utf16_target_len);
|
||||
assert_eq!(delta_a.utf16_target_len, delta_b.utf16_base_len);
|
||||
assert_eq!(delta_b.apply(&delta_a.apply(&s).unwrap()).unwrap(), s);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invert_test() {
|
||||
let s = "hello world";
|
||||
let delta = DeltaBuilder::new().insert(s).build();
|
||||
let invert_delta = delta.invert_str("");
|
||||
assert_eq!(delta.utf16_base_len, invert_delta.utf16_target_len);
|
||||
assert_eq!(delta.utf16_target_len, invert_delta.utf16_base_len);
|
||||
|
||||
assert_eq!(invert_delta.apply(s).unwrap(), "")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_ops() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.retain(0, AttributeHashMap::default());
|
||||
delta.insert("", AttributeHashMap::default());
|
||||
delta.delete(0);
|
||||
assert_eq!(delta.ops.len(), 0);
|
||||
}
|
||||
#[test]
|
||||
fn eq() {
|
||||
let mut delta_a = TextOperations::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();
|
||||
delta_b.delete(1);
|
||||
delta_b.insert("l", AttributeHashMap::default());
|
||||
delta_b.insert("o", AttributeHashMap::default());
|
||||
delta_b.retain(5, AttributeHashMap::default());
|
||||
assert_eq!(delta_a, delta_b);
|
||||
delta_a.delete(1);
|
||||
delta_b.retain(1, AttributeHashMap::default());
|
||||
assert_ne!(delta_a, delta_b);
|
||||
}
|
||||
#[test]
|
||||
fn ops_merging() {
|
||||
let mut delta = TextOperations::default();
|
||||
assert_eq!(delta.ops.len(), 0);
|
||||
delta.retain(2, AttributeHashMap::default());
|
||||
assert_eq!(delta.ops.len(), 1);
|
||||
assert_eq!(delta.ops.last(), Some(&DeltaOperation::retain(2)));
|
||||
delta.retain(3, AttributeHashMap::default());
|
||||
assert_eq!(delta.ops.len(), 1);
|
||||
assert_eq!(delta.ops.last(), Some(&DeltaOperation::retain(5)));
|
||||
delta.insert("abc", AttributeHashMap::default());
|
||||
assert_eq!(delta.ops.len(), 2);
|
||||
assert_eq!(delta.ops.last(), Some(&DeltaOperation::insert("abc")));
|
||||
delta.insert("xyz", AttributeHashMap::default());
|
||||
assert_eq!(delta.ops.len(), 2);
|
||||
assert_eq!(delta.ops.last(), Some(&DeltaOperation::insert("abcxyz")));
|
||||
delta.delete(1);
|
||||
assert_eq!(delta.ops.len(), 3);
|
||||
assert_eq!(delta.ops.last(), Some(&DeltaOperation::delete(1)));
|
||||
delta.delete(1);
|
||||
assert_eq!(delta.ops.len(), 3);
|
||||
assert_eq!(delta.ops.last(), Some(&DeltaOperation::delete(2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_noop() {
|
||||
let mut delta = TextOperations::default();
|
||||
assert!(delta.is_noop());
|
||||
delta.retain(5, AttributeHashMap::default());
|
||||
assert!(delta.is_noop());
|
||||
delta.retain(3, AttributeHashMap::default());
|
||||
assert!(delta.is_noop());
|
||||
delta.insert("lorem", AttributeHashMap::default());
|
||||
assert!(!delta.is_noop());
|
||||
}
|
||||
#[test]
|
||||
fn compose() {
|
||||
for _ in 0..1000 {
|
||||
let mut rng = Rng::default();
|
||||
let s = rng.gen_string(20);
|
||||
let a = rng.gen_delta(&s);
|
||||
let after_a: OTString = a.apply(&s).unwrap().into();
|
||||
assert_eq!(a.utf16_target_len, after_a.utf16_len());
|
||||
|
||||
let b = rng.gen_delta(&after_a);
|
||||
let after_b: OTString = b.apply(&after_a).unwrap().into();
|
||||
assert_eq!(b.utf16_target_len, after_b.utf16_len());
|
||||
|
||||
let ab = a.compose(&b).unwrap();
|
||||
assert_eq!(ab.utf16_target_len, b.utf16_target_len);
|
||||
let after_ab: OTString = ab.apply(&s).unwrap().into();
|
||||
assert_eq!(after_b, after_ab);
|
||||
}
|
||||
}
|
||||
#[test]
|
||||
fn transform_random_delta() {
|
||||
for _ in 0..1000 {
|
||||
let mut rng = Rng::default();
|
||||
let s = rng.gen_string(20);
|
||||
let a = rng.gen_delta(&s);
|
||||
let b = rng.gen_delta(&s);
|
||||
let (a_prime, b_prime) = a.transform(&b).unwrap();
|
||||
let ab_prime = a.compose(&b_prime).unwrap();
|
||||
let ba_prime = b.compose(&a_prime).unwrap();
|
||||
assert_eq!(ab_prime, ba_prime);
|
||||
|
||||
let after_ab_prime = ab_prime.apply(&s).unwrap();
|
||||
let after_ba_prime = ba_prime.apply(&s).unwrap();
|
||||
assert_eq!(after_ab_prime, after_ba_prime);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_with_two_delta() {
|
||||
let mut a = TextOperations::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_s = String::new();
|
||||
b.insert("456", AttributeHashMap::default());
|
||||
b_s = b.apply(&b_s).unwrap();
|
||||
assert_eq!(&b_s, "456");
|
||||
|
||||
let (a_prime, b_prime) = a.transform(&b).unwrap();
|
||||
assert_eq!(
|
||||
r#"[{"insert":"123","attributes":{"bold":true}},{"retain":3}]"#,
|
||||
serde_json::to_string(&a_prime).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
r#"[{"retain":3,"attributes":{"bold":true}},{"insert":"456"}]"#,
|
||||
serde_json::to_string(&b_prime).unwrap()
|
||||
);
|
||||
|
||||
let new_a = a.compose(&b_prime).unwrap();
|
||||
let new_b = b.compose(&a_prime).unwrap();
|
||||
assert_eq!(
|
||||
r#"[{"insert":"123","attributes":{"bold":true}},{"insert":"456"}]"#,
|
||||
serde_json::to_string(&new_a).unwrap()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
r#"[{"insert":"123","attributes":{"bold":true}},{"insert":"456"}]"#,
|
||||
serde_json::to_string(&new_b).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_two_plain_delta() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(1, "456", 0),
|
||||
Transform(0, 1),
|
||||
AssertDocJson(0, r#"[{"insert":"123456"}]"#),
|
||||
AssertDocJson(1, r#"[{"insert":"123456"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_two_plain_delta2() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(1, "456", 0),
|
||||
TransformPrime(0, 1),
|
||||
DocComposePrime(0, 1),
|
||||
DocComposePrime(1, 0),
|
||||
AssertDocJson(0, r#"[{"insert":"123456"}]"#),
|
||||
AssertDocJson(1, r#"[{"insert":"123456"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_two_non_seq_delta() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(1, "456", 0),
|
||||
TransformPrime(0, 1),
|
||||
AssertPrimeJson(0, r#"[{"insert":"123"},{"retain":3}]"#),
|
||||
AssertPrimeJson(1, r#"[{"retain":3},{"insert":"456"}]"#),
|
||||
DocComposePrime(0, 1),
|
||||
Insert(1, "78", 3),
|
||||
Insert(1, "9", 5),
|
||||
DocComposePrime(1, 0),
|
||||
AssertDocJson(0, r#"[{"insert":"123456"}]"#),
|
||||
AssertDocJson(1, r#"[{"insert":"123456789"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_two_conflict_non_seq_delta() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(1, "456", 0),
|
||||
TransformPrime(0, 1),
|
||||
DocComposePrime(0, 1),
|
||||
Insert(1, "78", 0),
|
||||
DocComposePrime(1, 0),
|
||||
AssertDocJson(0, r#"[{"insert":"123456"}]"#),
|
||||
AssertDocJson(1, r#"[{"insert":"12378456"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_invert_no_attribute_delta() {
|
||||
let mut delta = TextOperations::default();
|
||||
delta.add(DeltaOperation::insert("123"));
|
||||
|
||||
let mut change = TextOperations::default();
|
||||
change.add(DeltaOperation::retain(3));
|
||||
change.add(DeltaOperation::insert("456"));
|
||||
let undo = change.invert(&delta);
|
||||
|
||||
let new_delta = delta.compose(&change).unwrap();
|
||||
let delta_after_undo = new_delta.compose(&undo).unwrap();
|
||||
|
||||
assert_eq!(delta_after_undo, delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_invert_no_attribute_delta2() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(1, "4567", 0),
|
||||
Invert(0, 1),
|
||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_invert_attribute_delta_with_no_attribute_delta() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#),
|
||||
Insert(1, "4567", 0),
|
||||
Invert(0, 1),
|
||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_invert_attribute_delta_with_no_attribute_delta2() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Insert(0, "456", 3),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"123456","attributes":{"bold":true}}]
|
||||
"#,
|
||||
),
|
||||
Italic(0, Interval::new(2, 4), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"bold":true}},
|
||||
{"insert":"34","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"56","attributes":{"bold":true}}
|
||||
]"#,
|
||||
),
|
||||
Insert(1, "abc", 0),
|
||||
Invert(0, 1),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"bold":true}},
|
||||
{"insert":"34","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"56","attributes":{"bold":true}}
|
||||
]"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_invert_no_attribute_delta_with_attribute_delta() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(1, "4567", 0),
|
||||
Bold(1, Interval::new(0, 3), true),
|
||||
AssertDocJson(1, r#"[{"insert":"456","attributes":{"bold":true}},{"insert":"7"}]"#),
|
||||
Invert(0, 1),
|
||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_invert_no_attribute_delta_with_attribute_delta2() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
||||
Insert(1, "abc", 0),
|
||||
Bold(1, Interval::new(0, 3), true),
|
||||
Insert(1, "d", 3),
|
||||
Italic(1, Interval::new(1, 3), true),
|
||||
AssertDocJson(
|
||||
1,
|
||||
r#"[{"insert":"a","attributes":{"bold":true}},{"insert":"bc","attributes":{"bold":true,"italic":true}},{"insert":"d","attributes":{"bold":true}}]"#,
|
||||
),
|
||||
Invert(0, 1),
|
||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_invert_attribute_delta_with_attribute_delta() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Insert(0, "456", 3),
|
||||
AssertDocJson(0, r#"[{"insert":"123456","attributes":{"bold":true}}]"#),
|
||||
Italic(0, Interval::new(2, 4), true),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"bold":true}},
|
||||
{"insert":"34","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"56","attributes":{"bold":true}}
|
||||
]"#,
|
||||
),
|
||||
Insert(1, "abc", 0),
|
||||
Bold(1, Interval::new(0, 3), true),
|
||||
Insert(1, "d", 3),
|
||||
Italic(1, Interval::new(1, 3), true),
|
||||
AssertDocJson(
|
||||
1,
|
||||
r#"[
|
||||
{"insert":"a","attributes":{"bold":true}},
|
||||
{"insert":"bc","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"d","attributes":{"bold":true}}
|
||||
]"#,
|
||||
),
|
||||
Invert(0, 1),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"12","attributes":{"bold":true}},
|
||||
{"insert":"34","attributes":{"bold":true,"italic":true}},
|
||||
{"insert":"56","attributes":{"bold":true}}
|
||||
]"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_compose_str() {
|
||||
let ops = vec![
|
||||
Insert(0, "1", 0),
|
||||
Insert(0, "2", 1),
|
||||
AssertDocJson(0, r#"[{"insert":"12\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn delta_compose_with_missing_delta() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Insert(0, "4", 3),
|
||||
DocComposeDelta(1, 0),
|
||||
AssertDocJson(0, r#"[{"insert":"1234\n"}]"#),
|
||||
AssertStr(1, r#"4\n"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
113
frontend/rust-lib/flowy-document/tests/editor/serde_test.rs
Normal file
113
frontend/rust-lib/flowy-document/tests/editor/serde_test.rs
Normal file
@ -0,0 +1,113 @@
|
||||
use flowy_sync::client_document::{ClientDocument, EmptyDoc};
|
||||
use lib_ot::text_delta::TextOperation;
|
||||
use lib_ot::{
|
||||
core::*,
|
||||
text_delta::{BuildInTextAttribute, TextOperations},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn operation_insert_serialize_test() {
|
||||
let attributes = AttributeBuilder::new()
|
||||
.insert("bold", true)
|
||||
.insert("italic", true)
|
||||
.build();
|
||||
let operation = DeltaOperation::insert_with_attributes("123", attributes);
|
||||
let json = serde_json::to_string(&operation).unwrap();
|
||||
eprintln!("{}", json);
|
||||
|
||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(insert_op, operation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
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();
|
||||
assert_eq!(insert_op, operation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn operation_delete_serialize_test() {
|
||||
let operation = TextOperation::Delete(2);
|
||||
let json = serde_json::to_string(&operation).unwrap();
|
||||
let insert_op: TextOperation = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(insert_op, operation);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn attributes_serialize_test() {
|
||||
let attributes = AttributeBuilder::new()
|
||||
.insert_entry(BuildInTextAttribute::Bold(true))
|
||||
.insert_entry(BuildInTextAttribute::Italic(true))
|
||||
.build();
|
||||
let retain = DeltaOperation::insert_with_attributes("123", attributes);
|
||||
|
||||
let json = serde_json::to_string(&retain).unwrap();
|
||||
eprintln!("{}", json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_serialize_multi_attribute_test() {
|
||||
let mut delta = DeltaOperations::default();
|
||||
|
||||
let attributes = AttributeBuilder::new()
|
||||
.insert_entry(BuildInTextAttribute::Bold(true))
|
||||
.insert_entry(BuildInTextAttribute::Italic(true))
|
||||
.build();
|
||||
let retain = DeltaOperation::insert_with_attributes("123", attributes);
|
||||
|
||||
delta.add(retain);
|
||||
delta.add(DeltaOperation::Retain(5.into()));
|
||||
delta.add(DeltaOperation::Delete(3));
|
||||
|
||||
let json = serde_json::to_string(&delta).unwrap();
|
||||
eprintln!("{}", json);
|
||||
|
||||
let delta_from_json = DeltaOperations::from_json(&json).unwrap();
|
||||
assert_eq!(delta_from_json, delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_deserialize_test() {
|
||||
let json = r#"[
|
||||
{"retain":2,"attributes":{"italic":true}},
|
||||
{"retain":2,"attributes":{"italic":123}},
|
||||
{"retain":2,"attributes":{"italic":true,"bold":true}},
|
||||
{"retain":2,"attributes":{"italic":true,"bold":true}}
|
||||
]"#;
|
||||
let delta = TextOperations::from_json(json).unwrap();
|
||||
eprintln!("{}", delta);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delta_deserialize_null_test() {
|
||||
let json = r#"[
|
||||
{"retain":7,"attributes":{"bold":null}}
|
||||
]"#;
|
||||
let delta1 = TextOperations::from_json(json).unwrap();
|
||||
|
||||
let mut attribute = BuildInTextAttribute::Bold(true);
|
||||
attribute.remove_value();
|
||||
|
||||
let delta2 = OperationBuilder::new()
|
||||
.retain_with_attributes(7, attribute.into())
|
||||
.build();
|
||||
|
||||
assert_eq!(delta2.json_str(), r#"[{"retain":7,"attributes":{"bold":null}}]"#);
|
||||
assert_eq!(delta1, delta2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_insert_serde_test() {
|
||||
let mut document = ClientDocument::new::<EmptyDoc>();
|
||||
document.insert(0, "\n").unwrap();
|
||||
document.insert(0, "123").unwrap();
|
||||
let json = document.get_operations_json();
|
||||
assert_eq!(r#"[{"insert":"123\n"}]"#, json);
|
||||
assert_eq!(
|
||||
r#"[{"insert":"123\n"}]"#,
|
||||
ClientDocument::from_json(&json).unwrap().get_operations_json()
|
||||
);
|
||||
}
|
373
frontend/rust-lib/flowy-document/tests/editor/undo_redo_test.rs
Normal file
373
frontend/rust-lib/flowy-document/tests/editor/undo_redo_test.rs
Normal file
@ -0,0 +1,373 @@
|
||||
use crate::editor::{TestBuilder, TestOp::*};
|
||||
use flowy_sync::client_document::{EmptyDoc, NewlineDoc, RECORD_THRESHOLD};
|
||||
use lib_ot::core::{Interval, NEW_LINE, WHITESPACE};
|
||||
|
||||
#[test]
|
||||
fn history_insert_undo() {
|
||||
let ops = vec![Insert(0, "123", 0), Undo(0), AssertDocJson(0, r#"[{"insert":"\n"}]"#)];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_insert_undo_with_lagging() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Insert(0, "456", 0),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_insert_redo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
Redo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_insert_redo_with_lagging() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Insert(0, "456", 3),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
AssertStr(0, "123456\n"),
|
||||
AssertDocJson(0, r#"[{"insert":"123456\n"}]"#),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
|
||||
Redo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123456\n"}]"#),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_bold_undo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_bold_undo_with_lagging() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_bold_redo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
Redo(0),
|
||||
AssertDocJson(0, r#" [{"insert":"123","attributes":{"bold":true}},{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_bold_redo_with_lagging() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123\n"}]"#),
|
||||
Redo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}},{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_delete_undo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
||||
Delete(0, Interval::new(0, 3)),
|
||||
AssertDocJson(0, r#"[]"#),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<EmptyDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_delete_undo_2() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Delete(0, Interval::new(0, 1)),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"23","attributes":{"bold":true}},
|
||||
{"insert":"\n"}]
|
||||
"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_delete_undo_with_lagging() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Delete(0, Interval::new(0, 1)),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"23","attributes":{"bold":true}},
|
||||
{"insert":"\n"}]
|
||||
"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"123","attributes":{"bold":true}},
|
||||
{"insert":"\n"}]
|
||||
"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_delete_redo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Delete(0, Interval::new(0, 3)),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
Undo(0),
|
||||
Redo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_replace_undo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Replace(0, Interval::new(0, 2), "ab"),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"ab"},
|
||||
{"insert":"3","attributes":{"bold":true}},{"insert":"\n"}]
|
||||
"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_replace_undo_with_lagging() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Replace(0, Interval::new(0, 2), "ab"),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"ab"},
|
||||
{"insert":"3","attributes":{"bold":true}},{"insert":"\n"}]
|
||||
"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"123","attributes":{"bold":true}},{"insert":"\n"}]"#),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_replace_redo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123", 0),
|
||||
Bold(0, Interval::new(0, 3), true),
|
||||
Replace(0, Interval::new(0, 2), "ab"),
|
||||
Undo(0),
|
||||
Redo(0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[
|
||||
{"insert":"ab"},
|
||||
{"insert":"3","attributes":{"bold":true}},{"insert":"\n"}]
|
||||
"#,
|
||||
),
|
||||
];
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_header_added_undo() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Header(0, Interval::new(0, 6), 1),
|
||||
Insert(0, "\n", 3),
|
||||
Insert(0, "\n", 4),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
Redo(0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n\n","attributes":{"header":1}},{"insert":"456"},{"insert":"\n","attributes":{"header":1}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_link_added_undo() {
|
||||
let site = "https://appflowy.io";
|
||||
let ops = vec![
|
||||
Insert(0, site, 0),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Link(0, Interval::new(0, site.len()), site),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
|
||||
Redo(0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"https://appflowy.io","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_link_auto_format_undo_with_lagging() {
|
||||
let site = "https://appflowy.io";
|
||||
let ops = vec![
|
||||
Insert(0, site, 0),
|
||||
AssertDocJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Insert(0, WHITESPACE, site.len()),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"https://appflowy.io","attributes":{"link":"https://appflowy.io/"}},{"insert":" \n"}]"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_bullet_undo() {
|
||||
let ops = vec![
|
||||
Insert(0, "1", 0),
|
||||
Bullet(0, Interval::new(0, 1), true),
|
||||
Insert(0, NEW_LINE, 1),
|
||||
Insert(0, "2", 2),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"2"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
Redo(0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"2"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_bullet_undo_with_lagging() {
|
||||
let ops = vec![
|
||||
Insert(0, "1", 0),
|
||||
Bullet(0, Interval::new(0, 1), true),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Insert(0, NEW_LINE, 1),
|
||||
Insert(0, "2", 2),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"2"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}}]"#),
|
||||
Undo(0),
|
||||
AssertDocJson(0, r#"[{"insert":"\n"}]"#),
|
||||
Redo(0),
|
||||
Redo(0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"1"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"2"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn history_undo_attribute_on_merge_between_line() {
|
||||
let ops = vec![
|
||||
Insert(0, "123456", 0),
|
||||
Bullet(0, Interval::new(0, 6), true),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
Insert(0, NEW_LINE, 3),
|
||||
Wait(RECORD_THRESHOLD),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"456"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Delete(0, Interval::new(3, 4)), // delete the newline
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123456"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
Undo(0),
|
||||
AssertDocJson(
|
||||
0,
|
||||
r#"[{"insert":"123"},{"insert":"\n","attributes":{"list":"bullet"}},{"insert":"456"},{"insert":"\n","attributes":{"list":"bullet"}}]"#,
|
||||
),
|
||||
];
|
||||
|
||||
TestBuilder::new().run_scripts::<NewlineDoc>(ops);
|
||||
}
|
2
frontend/rust-lib/flowy-document/tests/main.rs
Normal file
2
frontend/rust-lib/flowy-document/tests/main.rs
Normal file
@ -0,0 +1,2 @@
|
||||
mod document;
|
||||
mod editor;
|
Reference in New Issue
Block a user