diff --git a/app_flowy/assets/images/home/Close.svg b/app_flowy/assets/images/home/Close.svg index f0733a398b..28b0cb04fb 100755 --- a/app_flowy/assets/images/home/Close.svg +++ b/app_flowy/assets/images/home/Close.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/app_flowy/assets/images/home/Favorite/Active.svg b/app_flowy/assets/images/home/Favorite/Active.svg new file mode 100755 index 0000000000..0f7ba51f4d --- /dev/null +++ b/app_flowy/assets/images/home/Favorite/Active.svg @@ -0,0 +1,3 @@ + + + diff --git a/app_flowy/assets/images/home/Favorite/Inactive.svg b/app_flowy/assets/images/home/Favorite/Inactive.svg new file mode 100755 index 0000000000..a6e84e8e91 --- /dev/null +++ b/app_flowy/assets/images/home/Favorite/Inactive.svg @@ -0,0 +1,3 @@ + + + diff --git a/app_flowy/assets/images/home/Image.svg b/app_flowy/assets/images/home/Image.svg new file mode 100755 index 0000000000..7a3cbef1b8 --- /dev/null +++ b/app_flowy/assets/images/home/Image.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/app_flowy/assets/images/home/Page.svg b/app_flowy/assets/images/home/Page.svg index 0846ad2335..ccf481ffcf 100755 --- a/app_flowy/assets/images/home/Page.svg +++ b/app_flowy/assets/images/home/Page.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart index 2158b6a53e..6bac6ea792 100644 --- a/app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart +++ b/app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart @@ -49,7 +49,7 @@ class Rules { // const PreserveBlockStyleOnInsertRule(), // const PreserveLineStyleOnSplitRule(), const ResetLineFormatOnNewLineRule(), - // const AutoFormatLinksRule(), + const AutoFormatLinksRule(), const PreserveInlineStylesRule(), const CatchAllInsertRule(), // const EnsureEmbedLineRule(), diff --git a/app_flowy/packages/flowy_editor/lib/src/service/controller.dart b/app_flowy/packages/flowy_editor/lib/src/service/controller.dart index e9f9339fa6..08688a76ae 100644 --- a/app_flowy/packages/flowy_editor/lib/src/service/controller.dart +++ b/app_flowy/packages/flowy_editor/lib/src/service/controller.dart @@ -93,7 +93,8 @@ class EditorController extends ChangeNotifier { toggledStyle = toggledStyle.put(attribute); } - final change = document.format(index, length, attribute); + final change = + document.format(index, length, LinkAttribute("www.baidu.com")); final adjustedSelection = selection.copyWith( baseOffset: change.transformPosition(selection.baseOffset), extentOffset: change.transformPosition(selection.extentOffset), diff --git a/rust-lib/flowy-ot/src/client/view/delete_ext.rs b/rust-lib/flowy-ot/src/client/extensions/delete/default_delete.rs similarity index 92% rename from rust-lib/flowy-ot/src/client/view/delete_ext.rs rename to rust-lib/flowy-ot/src/client/extensions/delete/default_delete.rs index 9cdb8c5cc5..2ddb980efb 100644 --- a/rust-lib/flowy-ot/src/client/view/delete_ext.rs +++ b/rust-lib/flowy-ot/src/client/extensions/delete/default_delete.rs @@ -1,5 +1,5 @@ use crate::{ - client::view::DeleteExt, + client::extensions::DeleteExt, core::{Delta, DeltaBuilder, Interval}, }; diff --git a/rust-lib/flowy-ot/src/client/extensions/delete/mod.rs b/rust-lib/flowy-ot/src/client/extensions/delete/mod.rs new file mode 100644 index 0000000000..b3cf97ef95 --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/delete/mod.rs @@ -0,0 +1,3 @@ +mod default_delete; + +pub use default_delete::*; diff --git a/rust-lib/flowy-ot/src/client/extensions/format/format_at_position.rs b/rust-lib/flowy-ot/src/client/extensions/format/format_at_position.rs new file mode 100644 index 0000000000..054b7f360f --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/format/format_at_position.rs @@ -0,0 +1,49 @@ +use crate::{ + client::extensions::FormatExt, + core::{Attribute, AttributeKey, CharMetric, Delta, DeltaBuilder, DeltaIter, Interval}, +}; + +pub struct FormatLinkAtCaretPositionExt {} + +impl FormatExt for FormatLinkAtCaretPositionExt { + fn ext_name(&self) -> &str { "FormatLinkAtCaretPositionExt" } + + fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option { + if attribute.key != AttributeKey::Link || interval.size() != 0 { + return None; + } + + let mut iter = DeltaIter::new(delta); + iter.seek::(interval.start); + + let (before, after) = (iter.next_op_before(interval.size()), iter.next()); + let mut start = interval.end; + let mut retain = 0; + + if let Some(before) = before { + if before.contain_attribute(attribute) { + start -= before.len(); + retain += before.len(); + } + } + + if let Some(after) = after { + if after.contain_attribute(attribute) { + if retain != 0 { + retain += after.len(); + } + } + } + + if retain == 0 { + return None; + } + + Some( + DeltaBuilder::new() + .retain(start) + .retain_with_attributes(retain, (attribute.clone()).into()) + .build(), + ) + } +} diff --git a/rust-lib/flowy-ot/src/client/extensions/format/helper.rs b/rust-lib/flowy-ot/src/client/extensions/format/helper.rs new file mode 100644 index 0000000000..296ee24c1a --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/format/helper.rs @@ -0,0 +1,39 @@ +use crate::{ + client::util::find_newline, + core::{Attribute, AttributeScope, Attributes, Delta, Operation}, +}; + +pub(crate) fn line_break(op: &Operation, attribute: &Attribute, scope: AttributeScope) -> Delta { + let mut new_delta = Delta::new(); + let mut start = 0; + let end = op.len(); + let mut s = op.get_data(); + + while let Some(line_break) = find_newline(s) { + match scope { + AttributeScope::Inline => { + new_delta.retain(line_break - start, attribute.clone().into()); + new_delta.retain(1, Attributes::empty()); + }, + AttributeScope::Block => { + new_delta.retain(line_break - start, Attributes::empty()); + new_delta.retain(1, attribute.clone().into()); + }, + _ => { + log::error!("Unsupported parser line break for {:?}", scope); + }, + } + + start = line_break + 1; + s = &s[start..s.len()]; + } + + if start < end { + match scope { + AttributeScope::Inline => new_delta.retain(end - start, attribute.clone().into()), + AttributeScope::Block => new_delta.retain(end - start, Attributes::empty()), + _ => log::error!("Unsupported parser line break for {:?}", scope), + } + } + new_delta +} diff --git a/rust-lib/flowy-ot/src/client/extensions/format/mod.rs b/rust-lib/flowy-ot/src/client/extensions/format/mod.rs new file mode 100644 index 0000000000..962b357776 --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/format/mod.rs @@ -0,0 +1,8 @@ +mod format_at_position; +mod helper; +mod resolve_block_format; +mod resolve_inline_format; + +pub use format_at_position::*; +pub use resolve_block_format::*; +pub use resolve_inline_format::*; diff --git a/rust-lib/flowy-ot/src/client/extensions/format/resolve_block_format.rs b/rust-lib/flowy-ot/src/client/extensions/format/resolve_block_format.rs new file mode 100644 index 0000000000..5d29bbad11 --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/format/resolve_block_format.rs @@ -0,0 +1,62 @@ +use crate::{ + client::{ + extensions::{format::helper::line_break, FormatExt}, + util::find_newline, + }, + core::{ + Attribute, + AttributeScope, + Attributes, + CharMetric, + Delta, + DeltaBuilder, + DeltaIter, + Interval, + }, +}; + +pub struct ResolveBlockFormatExt {} +impl FormatExt for ResolveBlockFormatExt { + fn ext_name(&self) -> &str { "ResolveBlockFormatExt" } + + fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option { + if attribute.scope != AttributeScope::Block { + return None; + } + + let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); + let mut iter = DeltaIter::new(delta); + iter.seek::(interval.start); + let mut start = 0; + let end = interval.size(); + while start < end && iter.has_next() { + let next_op = iter.next_op_before(end - start).unwrap(); + match find_newline(next_op.get_data()) { + None => new_delta.retain(next_op.len(), Attributes::empty()), + Some(_) => { + let tmp_delta = line_break(&next_op, attribute, AttributeScope::Block); + new_delta.extend(tmp_delta); + }, + } + + start += next_op.len(); + } + + while iter.has_next() { + let op = iter + .next_op() + .expect("Unexpected None, iter.has_next() must return op"); + + match find_newline(op.get_data()) { + None => new_delta.retain(op.len(), Attributes::empty()), + Some(line_break) => { + debug_assert_eq!(line_break, 0); + new_delta.retain(1, attribute.clone().into()); + break; + }, + } + } + + Some(new_delta) + } +} diff --git a/rust-lib/flowy-ot/src/client/extensions/format/resolve_inline_format.rs b/rust-lib/flowy-ot/src/client/extensions/format/resolve_inline_format.rs new file mode 100644 index 0000000000..8b51dec78b --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/format/resolve_inline_format.rs @@ -0,0 +1,39 @@ +use crate::{ + client::{ + extensions::{format::helper::line_break, FormatExt}, + util::find_newline, + }, + core::{Attribute, AttributeScope, CharMetric, Delta, DeltaBuilder, DeltaIter, Interval}, +}; + +pub struct ResolveInlineFormatExt {} +impl FormatExt for ResolveInlineFormatExt { + fn ext_name(&self) -> &str { "ResolveInlineFormatExt" } + + fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option { + if attribute.scope != AttributeScope::Inline { + return None; + } + let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); + let mut iter = DeltaIter::new(delta); + iter.seek::(interval.start); + + let mut start = 0; + let end = interval.size(); + + while start < end && iter.has_next() { + let next_op = iter.next_op_before(end - start).unwrap(); + match find_newline(next_op.get_data()) { + None => new_delta.retain(next_op.len(), attribute.clone().into()), + Some(_) => { + let tmp_delta = line_break(&next_op, attribute, AttributeScope::Inline); + new_delta.extend(tmp_delta); + }, + } + + start += next_op.len(); + } + + Some(new_delta) + } +} diff --git a/rust-lib/flowy-ot/src/client/extensions/insert/auto_format.rs b/rust-lib/flowy-ot/src/client/extensions/insert/auto_format.rs new file mode 100644 index 0000000000..2ed1eea6e6 --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/insert/auto_format.rs @@ -0,0 +1,26 @@ +use crate::{ + client::{extensions::InsertExt, util::is_whitespace}, + core::{CharMetric, Delta, DeltaIter}, +}; + +pub struct AutoFormatExt {} +impl InsertExt for AutoFormatExt { + fn ext_name(&self) -> &str { "AutoFormatExt" } + + fn apply(&self, delta: &Delta, _replace_len: usize, text: &str, index: usize) -> Option { + // enter whitespace to trigger auto format + if !is_whitespace(text) { + return None; + } + let mut iter = DeltaIter::new(delta); + iter.seek::(index); + let prev = iter.next_op(); + if prev.is_none() { + return None; + } + + let _prev = prev.unwrap(); + + None + } +} diff --git a/rust-lib/flowy-ot/src/client/extensions/insert/default_insert.rs b/rust-lib/flowy-ot/src/client/extensions/insert/default_insert.rs new file mode 100644 index 0000000000..813c173790 --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/insert/default_insert.rs @@ -0,0 +1,37 @@ +use crate::{ + client::extensions::{InsertExt, NEW_LINE}, + core::{AttributeKey, Attributes, Delta, DeltaBuilder, DeltaIter}, +}; + +pub struct DefaultInsertExt {} +impl InsertExt for DefaultInsertExt { + fn ext_name(&self) -> &str { "DefaultInsertExt" } + + fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option { + let iter = DeltaIter::new(delta); + let mut attributes = Attributes::new(); + + // Enable each line split by "\n" remains the block attributes. for example: + // insert "\n" to "123456" at index 3 + // + // [{"insert":"123"},{"insert":"\n","attributes":{"header":"1"}}, + // {"insert":"456"},{"insert":"\n","attributes":{"header":"1"}}] + if text.ends_with(NEW_LINE) { + match iter.last() { + None => {}, + Some(op) => { + if op.get_attributes().contains_key(&AttributeKey::Header) { + attributes.extend(op.get_attributes()); + } + }, + } + } + + Some( + DeltaBuilder::new() + .retain(index + replace_len) + .insert_with_attributes(text, attributes) + .build(), + ) + } +} diff --git a/rust-lib/flowy-ot/src/client/extensions/insert/mod.rs b/rust-lib/flowy-ot/src/client/extensions/insert/mod.rs new file mode 100644 index 0000000000..dba868f89c --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/insert/mod.rs @@ -0,0 +1,90 @@ +pub use auto_format::*; +pub use default_insert::*; +pub use preserve_inline_style::*; +pub use reset_format_on_new_line::*; + +mod auto_format; +mod default_insert; +mod preserve_inline_style; +mod reset_format_on_new_line; + +use crate::{ + client::extensions::InsertExt, + core::{Delta, DeltaBuilder, DeltaIter, Operation}, +}; + +pub struct PreserveBlockStyleOnInsertExt {} +impl InsertExt for PreserveBlockStyleOnInsertExt { + fn ext_name(&self) -> &str { "PreserveBlockStyleOnInsertExt" } + + fn apply( + &self, + _delta: &Delta, + _replace_len: usize, + _text: &str, + _index: usize, + ) -> Option { + None + } +} + +pub struct PreserveLineStyleOnSplitExt {} +impl InsertExt for PreserveLineStyleOnSplitExt { + fn ext_name(&self) -> &str { "PreserveLineStyleOnSplitExt" } + + fn apply( + &self, + _delta: &Delta, + _replace_len: usize, + _text: &str, + _index: usize, + ) -> Option { + None + } +} + +pub struct AutoExitBlockExt {} + +impl InsertExt for AutoExitBlockExt { + fn ext_name(&self) -> &str { "AutoExitBlockExt" } + + fn apply( + &self, + _delta: &Delta, + _replace_len: usize, + _text: &str, + _index: usize, + ) -> Option { + None + } +} + +pub struct InsertEmbedsExt {} +impl InsertExt for InsertEmbedsExt { + fn ext_name(&self) -> &str { "InsertEmbedsExt" } + + fn apply( + &self, + _delta: &Delta, + _replace_len: usize, + _text: &str, + _index: usize, + ) -> Option { + None + } +} + +pub struct ForceNewlineForInsertsAroundEmbedExt {} +impl InsertExt for ForceNewlineForInsertsAroundEmbedExt { + fn ext_name(&self) -> &str { "ForceNewlineForInsertsAroundEmbedExt" } + + fn apply( + &self, + _delta: &Delta, + _replace_len: usize, + _text: &str, + _index: usize, + ) -> Option { + None + } +} diff --git a/rust-lib/flowy-ot/src/client/extensions/insert/preserve_inline_style.rs b/rust-lib/flowy-ot/src/client/extensions/insert/preserve_inline_style.rs new file mode 100644 index 0000000000..839924c5f0 --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/insert/preserve_inline_style.rs @@ -0,0 +1,51 @@ +use crate::{ + client::{ + extensions::InsertExt, + util::{contain_newline, OpNewline}, + }, + core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter}, +}; + +pub struct PreserveInlineStylesExt {} +impl InsertExt for PreserveInlineStylesExt { + fn ext_name(&self) -> &str { "PreserveInlineStylesExt" } + + fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option { + if contain_newline(text) { + return None; + } + + let mut iter = DeltaIter::new(delta); + let prev = iter.next_op_before(index)?; + if OpNewline::parse(&prev).is_contain() { + return None; + } + + let mut attributes = prev.get_attributes(); + if attributes.is_empty() || !attributes.contains_key(&AttributeKey::Link) { + return Some( + DeltaBuilder::new() + .retain(index + replace_len) + .insert_with_attributes(text, attributes) + .build(), + ); + } + + let next = iter.next_op(); + match &next { + None => attributes = Attributes::empty(), + Some(next) => { + if OpNewline::parse(&next).is_equal() { + attributes = Attributes::empty(); + } + }, + } + + let new_delta = DeltaBuilder::new() + .retain(index + replace_len) + .insert_with_attributes(text, attributes) + .build(); + + return Some(new_delta); + } +} diff --git a/rust-lib/flowy-ot/src/client/extensions/insert/reset_format_on_new_line.rs b/rust-lib/flowy-ot/src/client/extensions/insert/reset_format_on_new_line.rs new file mode 100644 index 0000000000..31d9a937b9 --- /dev/null +++ b/rust-lib/flowy-ot/src/client/extensions/insert/reset_format_on_new_line.rs @@ -0,0 +1,40 @@ +use crate::{ + client::{ + extensions::{InsertExt, NEW_LINE}, + util::is_newline, + }, + core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter}, +}; + +pub struct ResetLineFormatOnNewLineExt {} +impl InsertExt for ResetLineFormatOnNewLineExt { + fn ext_name(&self) -> &str { "ResetLineFormatOnNewLineExt" } + + fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option { + if !is_newline(text) { + return None; + } + + let mut iter = DeltaIter::new(delta); + iter.seek::(index); + let next_op = iter.next()?; + if !next_op.get_data().starts_with(NEW_LINE) { + return None; + } + + let mut reset_attribute = Attributes::new(); + if next_op.get_attributes().contains_key(&AttributeKey::Header) { + reset_attribute.add(AttributeKey::Header.value("")); + } + + let len = index + replace_len; + Some( + DeltaBuilder::new() + .retain(len) + .insert_with_attributes(NEW_LINE, next_op.get_attributes()) + .retain_with_attributes(1, reset_attribute) + .trim() + .build(), + ) + } +} diff --git a/rust-lib/flowy-ot/src/client/view/extension.rs b/rust-lib/flowy-ot/src/client/extensions/mod.rs similarity index 82% rename from rust-lib/flowy-ot/src/client/view/extension.rs rename to rust-lib/flowy-ot/src/client/extensions/mod.rs index de1ad3dbd0..605c65dd30 100644 --- a/rust-lib/flowy-ot/src/client/view/extension.rs +++ b/rust-lib/flowy-ot/src/client/extensions/mod.rs @@ -1,5 +1,15 @@ +pub use delete::*; +pub use format::*; +pub use insert::*; + use crate::core::{Attribute, Delta, Interval}; +mod delete; +mod format; +mod insert; + +pub const NEW_LINE: &'static str = "\n"; + pub type InsertExtension = Box; pub type FormatExtension = Box; pub type DeleteExtension = Box; diff --git a/rust-lib/flowy-ot/src/client/mod.rs b/rust-lib/flowy-ot/src/client/mod.rs index 8b810e729b..6df4796051 100644 --- a/rust-lib/flowy-ot/src/client/mod.rs +++ b/rust-lib/flowy-ot/src/client/mod.rs @@ -2,5 +2,9 @@ mod document; mod history; mod view; +pub mod extensions; +mod util; + pub use document::*; pub use history::*; +pub use view::*; diff --git a/rust-lib/flowy-ot/src/client/util.rs b/rust-lib/flowy-ot/src/client/util.rs new file mode 100644 index 0000000000..468e1d747a --- /dev/null +++ b/rust-lib/flowy-ot/src/client/util.rs @@ -0,0 +1,66 @@ +use crate::{client::extensions::NEW_LINE, core::Operation}; + +#[inline] +pub fn find_newline(s: &str) -> Option { + match s.find(NEW_LINE) { + None => None, + Some(line_break) => Some(line_break), + } +} + +#[derive(PartialEq, Eq)] +pub enum OpNewline { + Start, + End, + Contain, + Equal, + NotFound, +} + +impl OpNewline { + pub fn parse(op: &Operation) -> OpNewline { + let s = op.get_data(); + + if s == NEW_LINE { + return OpNewline::Equal; + } + + if s.starts_with(NEW_LINE) { + return OpNewline::Start; + } + + if s.ends_with(NEW_LINE) { + return OpNewline::End; + } + + if s.contains(NEW_LINE) { + return OpNewline::Contain; + } + + OpNewline::NotFound + } + + pub fn is_start(&self) -> bool { self == &OpNewline::Start } + + pub fn is_end(&self) -> bool { self == &OpNewline::End } + + pub fn is_not_found(&self) -> bool { self == &OpNewline::NotFound } + + pub fn is_contain(&self) -> bool { + self.is_start() || self.is_end() || self == &OpNewline::Contain + } + + pub fn is_equal(&self) -> bool { self == &OpNewline::Equal } +} + +#[inline] +pub fn is_op_contains_newline(op: &Operation) -> bool { contain_newline(op.get_data()) } + +#[inline] +pub fn is_newline(s: &str) -> bool { s == NEW_LINE } + +#[inline] +pub fn is_whitespace(s: &str) -> bool { s == " " } + +#[inline] +pub fn contain_newline(s: &str) -> bool { s.contains(NEW_LINE) } diff --git a/rust-lib/flowy-ot/src/client/view/view.rs b/rust-lib/flowy-ot/src/client/view.rs similarity index 98% rename from rust-lib/flowy-ot/src/client/view/view.rs rename to rust-lib/flowy-ot/src/client/view.rs index 95e6c74d26..7c89ac83ab 100644 --- a/rust-lib/flowy-ot/src/client/view/view.rs +++ b/rust-lib/flowy-ot/src/client/view.rs @@ -1,5 +1,5 @@ +use super::extensions::*; use crate::{ - client::view::*, core::{Attribute, Delta, Interval}, errors::{ErrorBuilder, OTError, OTErrorCode}, }; @@ -86,7 +86,7 @@ fn construct_insert_exts() -> Vec { Box::new(PreserveBlockStyleOnInsertExt {}), Box::new(PreserveLineStyleOnSplitExt {}), Box::new(ResetLineFormatOnNewLineExt {}), - Box::new(AutoFormatLinksExt {}), + Box::new(AutoFormatExt {}), Box::new(PreserveInlineStylesExt {}), Box::new(DefaultInsertExt {}), ] diff --git a/rust-lib/flowy-ot/src/client/view/format_ext.rs b/rust-lib/flowy-ot/src/client/view/format_ext.rs deleted file mode 100644 index 0bd1789241..0000000000 --- a/rust-lib/flowy-ot/src/client/view/format_ext.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::{ - client::view::{util::find_newline, FormatExt}, - core::{ - Attribute, - AttributeKey, - AttributeScope, - Attributes, - CharMetric, - Delta, - DeltaBuilder, - DeltaIter, - Interval, - Operation, - }, -}; - -pub struct FormatLinkAtCaretPositionExt {} - -impl FormatExt for FormatLinkAtCaretPositionExt { - fn ext_name(&self) -> &str { "FormatLinkAtCaretPositionExt" } - - fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option { - if attribute.key != AttributeKey::Link || interval.size() != 0 { - return None; - } - - let mut iter = DeltaIter::new(delta); - iter.seek::(interval.start); - - let (before, after) = (iter.next_op_before(interval.size()), iter.next()); - let mut start = interval.end; - let mut retain = 0; - - if let Some(before) = before { - if before.contain_attribute(attribute) { - start -= before.len(); - retain += before.len(); - } - } - - if let Some(after) = after { - if after.contain_attribute(attribute) { - if retain != 0 { - retain += after.len(); - } - } - } - - if retain == 0 { - return None; - } - - Some( - DeltaBuilder::new() - .retain(start) - .retain_with_attributes(retain, (attribute.clone()).into()) - .build(), - ) - } -} - -pub struct ResolveBlockFormatExt {} -impl FormatExt for ResolveBlockFormatExt { - fn ext_name(&self) -> &str { "ResolveBlockFormatExt" } - - fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option { - if attribute.scope != AttributeScope::Block { - return None; - } - - let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); - let mut iter = DeltaIter::new(delta); - iter.seek::(interval.start); - let mut start = 0; - let end = interval.size(); - while start < end && iter.has_next() { - let next_op = iter.next_op_before(end - start).unwrap(); - match find_newline(next_op.get_data()) { - None => new_delta.retain(next_op.len(), Attributes::empty()), - Some(_) => { - let tmp_delta = line_break(&next_op, attribute, AttributeScope::Block); - new_delta.extend(tmp_delta); - }, - } - - start += next_op.len(); - } - - while iter.has_next() { - let op = iter - .next_op() - .expect("Unexpected None, iter.has_next() must return op"); - - match find_newline(op.get_data()) { - None => new_delta.retain(op.len(), Attributes::empty()), - Some(line_break) => { - debug_assert_eq!(line_break, 0); - new_delta.retain(1, attribute.clone().into()); - break; - }, - } - } - - Some(new_delta) - } -} - -pub struct ResolveInlineFormatExt {} -impl FormatExt for ResolveInlineFormatExt { - fn ext_name(&self) -> &str { "ResolveInlineFormatExt" } - - fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option { - if attribute.scope != AttributeScope::Inline { - return None; - } - let mut new_delta = DeltaBuilder::new().retain(interval.start).build(); - let mut iter = DeltaIter::new(delta); - iter.seek::(interval.start); - - let mut start = 0; - let end = interval.size(); - - while start < end && iter.has_next() { - let next_op = iter.next_op_before(end - start).unwrap(); - match find_newline(next_op.get_data()) { - None => new_delta.retain(next_op.len(), attribute.clone().into()), - Some(_) => { - let tmp_delta = line_break(&next_op, attribute, AttributeScope::Inline); - new_delta.extend(tmp_delta); - }, - } - - start += next_op.len(); - } - - Some(new_delta) - } -} - -fn line_break(op: &Operation, attribute: &Attribute, scope: AttributeScope) -> Delta { - let mut new_delta = Delta::new(); - let mut start = 0; - let end = op.len(); - let mut s = op.get_data(); - - while let Some(line_break) = find_newline(s) { - match scope { - AttributeScope::Inline => { - new_delta.retain(line_break - start, attribute.clone().into()); - new_delta.retain(1, Attributes::empty()); - }, - AttributeScope::Block => { - new_delta.retain(line_break - start, Attributes::empty()); - new_delta.retain(1, attribute.clone().into()); - }, - _ => { - log::error!("Unsupported parser line break for {:?}", scope); - }, - } - - start = line_break + 1; - s = &s[start..s.len()]; - } - - if start < end { - match scope { - AttributeScope::Inline => new_delta.retain(end - start, attribute.clone().into()), - AttributeScope::Block => new_delta.retain(end - start, Attributes::empty()), - _ => log::error!("Unsupported parser line break for {:?}", scope), - } - } - new_delta -} diff --git a/rust-lib/flowy-ot/src/client/view/insert_ext.rs b/rust-lib/flowy-ot/src/client/view/insert_ext.rs deleted file mode 100644 index ddcc96ffab..0000000000 --- a/rust-lib/flowy-ot/src/client/view/insert_ext.rs +++ /dev/null @@ -1,198 +0,0 @@ -use crate::{ - client::view::InsertExt, - core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter}, -}; - -pub const NEW_LINE: &'static str = "\n"; - -pub struct PreserveBlockStyleOnInsertExt {} -impl InsertExt for PreserveBlockStyleOnInsertExt { - fn ext_name(&self) -> &str { "PreserveBlockStyleOnInsertExt" } - - fn apply( - &self, - _delta: &Delta, - _replace_len: usize, - _text: &str, - _index: usize, - ) -> Option { - None - } -} - -pub struct PreserveLineStyleOnSplitExt {} -impl InsertExt for PreserveLineStyleOnSplitExt { - fn ext_name(&self) -> &str { "PreserveLineStyleOnSplitExt" } - - fn apply( - &self, - _delta: &Delta, - _replace_len: usize, - _text: &str, - _index: usize, - ) -> Option { - None - } -} - -pub struct AutoExitBlockExt {} - -impl InsertExt for AutoExitBlockExt { - fn ext_name(&self) -> &str { "AutoExitBlockExt" } - - fn apply( - &self, - _delta: &Delta, - _replace_len: usize, - _text: &str, - _index: usize, - ) -> Option { - None - } -} - -pub struct InsertEmbedsExt {} -impl InsertExt for InsertEmbedsExt { - fn ext_name(&self) -> &str { "InsertEmbedsExt" } - - fn apply( - &self, - _delta: &Delta, - _replace_len: usize, - _text: &str, - _index: usize, - ) -> Option { - None - } -} - -pub struct ForceNewlineForInsertsAroundEmbedExt {} -impl InsertExt for ForceNewlineForInsertsAroundEmbedExt { - fn ext_name(&self) -> &str { "ForceNewlineForInsertsAroundEmbedExt" } - - fn apply( - &self, - _delta: &Delta, - _replace_len: usize, - _text: &str, - _index: usize, - ) -> Option { - None - } -} - -pub struct AutoFormatLinksExt {} -impl InsertExt for AutoFormatLinksExt { - fn ext_name(&self) -> &str { "AutoFormatLinksExt" } - - fn apply( - &self, - _delta: &Delta, - _replace_len: usize, - _text: &str, - _index: usize, - ) -> Option { - None - } -} - -pub struct PreserveInlineStylesExt {} -impl InsertExt for PreserveInlineStylesExt { - fn ext_name(&self) -> &str { "PreserveInlineStylesExt" } - - fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option { - if text.contains(NEW_LINE) { - return None; - } - - let mut iter = DeltaIter::new(delta); - let prev = iter.next_op_before(index)?; - if prev.get_data().contains(NEW_LINE) { - return None; - } - - let mut attributes = prev.get_attributes(); - if attributes.is_empty() || !attributes.contains_key(&AttributeKey::Link) { - return Some( - DeltaBuilder::new() - .retain(index + replace_len) - .insert_with_attributes(text, attributes) - .build(), - ); - } - - attributes.remove(&AttributeKey::Link); - let new_delta = DeltaBuilder::new() - .retain(index + replace_len) - .insert_with_attributes(text, attributes) - .build(); - - return Some(new_delta); - } -} - -pub struct ResetLineFormatOnNewLineExt {} -impl InsertExt for ResetLineFormatOnNewLineExt { - fn ext_name(&self) -> &str { "ResetLineFormatOnNewLineExt" } - - fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option { - if text != NEW_LINE { - return None; - } - - let mut iter = DeltaIter::new(delta); - iter.seek::(index); - let next_op = iter.next()?; - if !next_op.get_data().starts_with(NEW_LINE) { - return None; - } - - let mut reset_attribute = Attributes::new(); - if next_op.get_attributes().contains_key(&AttributeKey::Header) { - reset_attribute.add(AttributeKey::Header.value("")); - } - - let len = index + replace_len; - Some( - DeltaBuilder::new() - .retain(len) - .insert_with_attributes(NEW_LINE, next_op.get_attributes()) - .retain_with_attributes(1, reset_attribute) - .trim() - .build(), - ) - } -} - -pub struct DefaultInsertExt {} -impl InsertExt for DefaultInsertExt { - fn ext_name(&self) -> &str { "DefaultInsertExt" } - - fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option { - let iter = DeltaIter::new(delta); - let mut attributes = Attributes::new(); - - // Enable each line split by "\n" remains the block attributes. for example: - // insert "\n" to "123456" at index 3 - // - // [{"insert":"123"},{"insert":"\n","attributes":{"header":"1"}}, - // {"insert":"456"},{"insert":"\n","attributes":{"header":"1"}}] - if text.ends_with(NEW_LINE) { - match iter.last() { - None => {}, - Some(op) => { - if op.get_attributes().contains_key(&AttributeKey::Header) { - attributes.extend(op.get_attributes()); - } - }, - } - } - - Some( - DeltaBuilder::new() - .retain(index + replace_len) - .insert_with_attributes(text, attributes) - .build(), - ) - } -} diff --git a/rust-lib/flowy-ot/src/client/view/mod.rs b/rust-lib/flowy-ot/src/client/view/mod.rs deleted file mode 100644 index 0dbad27ebc..0000000000 --- a/rust-lib/flowy-ot/src/client/view/mod.rs +++ /dev/null @@ -1,12 +0,0 @@ -mod delete_ext; -mod extension; -mod format_ext; -mod insert_ext; -mod util; -mod view; - -pub use delete_ext::*; -pub use extension::*; -pub use format_ext::*; -pub use insert_ext::*; -pub use view::*; diff --git a/rust-lib/flowy-ot/src/client/view/util.rs b/rust-lib/flowy-ot/src/client/view/util.rs deleted file mode 100644 index 86f99ec162..0000000000 --- a/rust-lib/flowy-ot/src/client/view/util.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::client::view::NEW_LINE; - -pub fn find_newline(s: &str) -> Option { - match s.find(NEW_LINE) { - None => None, - Some(line_break) => Some(line_break), - } -} diff --git a/rust-lib/flowy-ot/tests/attribute_test.rs b/rust-lib/flowy-ot/tests/attribute_test.rs index a155389a88..3aa7ab7f49 100644 --- a/rust-lib/flowy-ot/tests/attribute_test.rs +++ b/rust-lib/flowy-ot/tests/attribute_test.rs @@ -3,6 +3,8 @@ pub mod helper; use crate::helper::{TestOp::*, *}; use flowy_ot::core::Interval; +use flowy_ot::client::extensions::NEW_LINE; + #[test] fn attributes_insert_text() { let ops = vec![ @@ -446,7 +448,7 @@ fn attributes_replace_with_text() { } #[test] -fn attributes_add_header() { +fn attributes_header_insert_newline_at_middle() { let ops = vec![ Insert(0, "123456", 0), Header(0, Interval::new(0, 6), 1, true), @@ -465,22 +467,7 @@ fn attributes_add_header() { } #[test] -fn attributes_header_add_newline() { - let ops = vec![ - Insert(0, "123456", 0), - Header(0, Interval::new(0, 6), 1, true), - Insert(0, "\n", 6), - AssertOpsJson( - 0, - r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":"1"}},{"insert":"\n"}]"#, - ), - ]; - - OpTester::new().run_script_with_newline(ops); -} - -#[test] -fn attributes_header_add_newline_2() { +fn attributes_header_insert_newline_at_middle2() { let ops = vec![ Insert(0, "123456", 0), Header(0, Interval::new(0, 6), 1, true), @@ -503,3 +490,100 @@ fn attributes_header_add_newline_2() { OpTester::new().run_script_with_newline(ops); } + +#[test] +fn attributes_header_insert_newline_at_trailing() { + let ops = vec![ + Insert(0, "123456", 0), + Header(0, Interval::new(0, 6), 1, true), + Insert(0, "\n", 6), + AssertOpsJson( + 0, + r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":"1"}},{"insert":"\n"}]"#, + ), + ]; + + OpTester::new().run_script_with_newline(ops); +} + +#[test] +fn attributes_add_link() { + let ops = vec![ + Insert(0, "123456", 0), + Link(0, Interval::new(0, 6), "https://appflowy.io", true), + AssertOpsJson( + 0, + r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#, + ), + ]; + + OpTester::new().run_script_with_newline(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", true), + AssertOpsJson( + 0, + r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#, + ), + Insert(0, "a", 0), + AssertOpsJson( + 0, + r#"[{"insert":"a"},{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#, + ), + ]; + + OpTester::new().run_script_with_newline(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", true), + Insert(0, "34", 2), + AssertOpsJson( + 0, + r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#, + ), + ]; + + OpTester::new().run_script_with_newline(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", true), + AssertOpsJson( + 0, + r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#, + ), + Insert(0, "a", 6), + AssertOpsJson( + 0, + r#"[{"insert":"123456","attributes":{"link":"https://appflowy.io"}},{"insert":"a\n"}]"#, + ), + ]; + + OpTester::new().run_script_with_newline(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", true), + Insert(0, NEW_LINE, 3), + AssertOpsJson( + 0, + r#"[{"insert":"123","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"},{"insert":"456","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#, + ), + ]; + + OpTester::new().run_script_with_newline(ops); +} diff --git a/rust-lib/flowy-ot/tests/helper/mod.rs b/rust-lib/flowy-ot/tests/helper/mod.rs index 2ccc5db34f..03567795ae 100644 --- a/rust-lib/flowy-ot/tests/helper/mod.rs +++ b/rust-lib/flowy-ot/tests/helper/mod.rs @@ -28,6 +28,9 @@ pub enum TestOp { #[display(fmt = "Header")] Header(usize, Interval, usize, bool), + #[display(fmt = "Link")] + Link(usize, Interval, &'static str, bool), + #[display(fmt = "Transform")] Transform(usize, usize), @@ -74,44 +77,52 @@ impl OpTester { let document = &mut self.documents[*delta_i]; document.insert(*index, s).unwrap(); }, - TestOp::Delete(delta_i, interval) => { + TestOp::Delete(delta_i, iv) => { let document = &mut self.documents[*delta_i]; - document.replace(*interval, "").unwrap(); + document.replace(*iv, "").unwrap(); }, - TestOp::Replace(delta_i, interval, s) => { + TestOp::Replace(delta_i, iv, s) => { let document = &mut self.documents[*delta_i]; - document.replace(*interval, s).unwrap(); + document.replace(*iv, s).unwrap(); }, - TestOp::InsertBold(delta_i, s, interval) => { + TestOp::InsertBold(delta_i, s, iv) => { let document = &mut self.documents[*delta_i]; - document.insert(interval.start, s).unwrap(); + document.insert(iv.start, s).unwrap(); document - .format(*interval, AttributeKey::Bold.value(true)) + .format(*iv, AttributeKey::Bold.value(true)) .unwrap(); }, - TestOp::Bold(delta_i, interval, enable) => { + TestOp::Bold(delta_i, iv, enable) => { let document = &mut self.documents[*delta_i]; let attribute = match *enable { true => AttributeKey::Bold.value(true), false => AttributeKey::Bold.remove(), }; - document.format(*interval, attribute).unwrap(); + document.format(*iv, attribute).unwrap(); }, - TestOp::Italic(delta_i, interval, enable) => { + TestOp::Italic(delta_i, iv, enable) => { let document = &mut self.documents[*delta_i]; let attribute = match *enable { true => AttributeKey::Italic.value("true"), false => AttributeKey::Italic.remove(), }; - document.format(*interval, attribute).unwrap(); + document.format(*iv, attribute).unwrap(); }, - TestOp::Header(delta_i, interval, level, enable) => { + TestOp::Header(delta_i, iv, level, enable) => { let document = &mut self.documents[*delta_i]; let attribute = match *enable { true => AttributeKey::Header.value(level), false => AttributeKey::Header.remove(), }; - document.format(*interval, attribute).unwrap(); + document.format(*iv, attribute).unwrap(); + }, + TestOp::Link(delta_i, iv, link, enable) => { + let document = &mut self.documents[*delta_i]; + let attribute = match *enable { + true => AttributeKey::Link.value(link.to_owned()), + false => AttributeKey::Link.remove(), + }; + document.format(*iv, attribute).unwrap(); }, TestOp::Transform(delta_a_i, delta_b_i) => { let (a_prime, b_prime) = self.documents[*delta_a_i] diff --git a/rust-lib/flowy-ot/tests/undo_redo_test.rs b/rust-lib/flowy-ot/tests/undo_redo_test.rs index 03f91a130e..e688aeb93c 100644 --- a/rust-lib/flowy-ot/tests/undo_redo_test.rs +++ b/rust-lib/flowy-ot/tests/undo_redo_test.rs @@ -4,7 +4,7 @@ use crate::helper::{TestOp::*, *}; use flowy_ot::{client::RECORD_THRESHOLD, core::Interval}; #[test] -fn delta_undo_insert() { +fn history_undo_insert() { let ops = vec![ Insert(0, "123", 0), Undo(0), @@ -14,7 +14,7 @@ fn delta_undo_insert() { } #[test] -fn delta_undo_insert2() { +fn history_undo_insert2() { let ops = vec![ Insert(0, "123", 0), Wait(RECORD_THRESHOLD), @@ -28,7 +28,7 @@ fn delta_undo_insert2() { } #[test] -fn delta_redo_insert() { +fn history_redo_insert() { let ops = vec![ Insert(0, "123", 0), AssertOpsJson(0, r#"[{"insert":"123\n"}]"#), @@ -41,7 +41,7 @@ fn delta_redo_insert() { } #[test] -fn delta_redo_insert_with_lagging() { +fn history_redo_insert_with_lagging() { let ops = vec![ Insert(0, "123", 0), Wait(RECORD_THRESHOLD), @@ -60,7 +60,7 @@ fn delta_redo_insert_with_lagging() { } #[test] -fn delta_undo_attributes() { +fn history_undo_attributes() { let ops = vec![ Insert(0, "123", 0), Bold(0, Interval::new(0, 3), true), @@ -71,7 +71,7 @@ fn delta_undo_attributes() { } #[test] -fn delta_undo_attributes_with_lagging() { +fn history_undo_attributes_with_lagging() { let ops = vec![ Insert(0, "123", 0), Wait(RECORD_THRESHOLD), @@ -83,7 +83,7 @@ fn delta_undo_attributes_with_lagging() { } #[test] -fn delta_redo_attributes() { +fn history_redo_attributes() { let ops = vec![ Insert(0, "123", 0), Bold(0, Interval::new(0, 3), true), @@ -99,7 +99,7 @@ fn delta_redo_attributes() { } #[test] -fn delta_redo_attributes_with_lagging() { +fn history_redo_attributes_with_lagging() { let ops = vec![ Insert(0, "123", 0), Wait(RECORD_THRESHOLD), @@ -116,7 +116,7 @@ fn delta_redo_attributes_with_lagging() { } #[test] -fn delta_undo_delete() { +fn history_undo_delete() { let ops = vec![ Insert(0, "123", 0), AssertOpsJson(0, r#"[{"insert":"123"}]"#), @@ -129,7 +129,7 @@ fn delta_undo_delete() { } #[test] -fn delta_undo_delete2() { +fn history_undo_delete2() { let ops = vec![ Insert(0, "123", 0), Bold(0, Interval::new(0, 3), true), @@ -148,7 +148,7 @@ fn delta_undo_delete2() { } #[test] -fn delta_undo_delete2_with_lagging() { +fn history_undo_delete2_with_lagging() { let ops = vec![ Insert(0, "123", 0), Wait(RECORD_THRESHOLD), @@ -175,7 +175,7 @@ fn delta_undo_delete2_with_lagging() { } #[test] -fn delta_redo_delete() { +fn history_redo_delete() { let ops = vec![ Insert(0, "123", 0), Wait(RECORD_THRESHOLD), @@ -189,7 +189,7 @@ fn delta_redo_delete() { } #[test] -fn delta_undo_replace() { +fn history_undo_replace() { let ops = vec![ Insert(0, "123", 0), Bold(0, Interval::new(0, 3), true), @@ -208,7 +208,7 @@ fn delta_undo_replace() { } #[test] -fn delta_undo_replace_with_lagging() { +fn history_undo_replace_with_lagging() { let ops = vec![ Insert(0, "123", 0), Wait(RECORD_THRESHOLD), @@ -232,7 +232,7 @@ fn delta_undo_replace_with_lagging() { } #[test] -fn delta_redo_replace() { +fn history_redo_replace() { let ops = vec![ Insert(0, "123", 0), Bold(0, Interval::new(0, 3), true), @@ -249,3 +249,21 @@ fn delta_redo_replace() { ]; OpTester::new().run_script_with_newline(ops); } + +#[test] +fn history_undo_add_header() { + let ops = vec![ + Insert(0, "123456", 0), + Header(0, Interval::new(0, 6), 1, true), + Insert(0, "\n", 3), + Insert(0, "\n", 4), + Undo(0), + Redo(0), + AssertOpsJson( + 0, + r#"[{"insert":"123"},{"insert":"\n\n","attributes":{"header":"1"}},{"insert":"456"},{"insert":"\n","attributes":{"header":"1"}}]"#, + ), + ]; + + OpTester::new().run_script_with_newline(ops); +}