Refactor/rename crate (#1275)

This commit is contained in:
Nathan.fooo
2022-10-13 23:29:37 +08:00
committed by GitHub
parent 48bb80b1d0
commit cf4a2920f8
68 changed files with 857 additions and 723 deletions

View 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"]

View 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"]

View 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);
}

View 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());
}
}

View 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,
}

View 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(&params.view_id).await?;
let operations_str = editor.get_operation_str().await?;
data_result(ExportDataPB {
data: operations_str,
export_type: params.export_type,
})
}

View 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,
}

View 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>;
}

View 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(&params.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);
})
}
});
}

View 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)
}
}

View 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)
})
}
}

View File

@ -0,0 +1,2 @@
mod script;
mod text_block_test;

View 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;
}
}

View File

@ -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;
}

View 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);
}

View 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
}
}

View 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);
}

View 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()
);
}

View 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);
}

View File

@ -0,0 +1,2 @@
mod document;
mod editor;