瀏覽代碼

config auto exit block extension

appflowy 3 年之前
父節點
當前提交
9bc72d3b9e

+ 1 - 1
app_flowy/packages/flowy_editor/lib/src/model/heuristic/rule.dart

@@ -45,7 +45,7 @@ class Rules {
     const ResolveInlineFormatRule(),
     // const InsertEmbedsRule(),
     // const ForceNewlineForInsertsAroundEmbedRule(),
-    // const AutoExitBlockRule(),
+    const AutoExitBlockRule(),
     // const PreserveBlockStyleOnInsertRule(),
     // const PreserveLineStyleOnSplitRule(),
     const ResetLineFormatOnNewLineRule(),

+ 1 - 2
app_flowy/packages/flowy_editor/lib/src/service/controller.dart

@@ -93,8 +93,7 @@ class EditorController extends ChangeNotifier {
       toggledStyle = toggledStyle.put(attribute);
     }
 
-    final change =
-        document.format(index, length, LinkAttribute("www.baidu.com"));
+    final change = document.format(index, length, attribute);
     final adjustedSelection = selection.copyWith(
       baseOffset: change.transformPosition(selection.baseOffset),
       extentOffset: change.transformPosition(selection.extentOffset),

+ 23 - 0
rust-lib/flowy-ot/src/client/extensions/insert/auto_exit_block.rs

@@ -0,0 +1,23 @@
+use crate::{
+    client::{extensions::InsertExt, util::is_newline},
+    core::{Delta, DeltaIter},
+};
+
+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<Delta> {
+        // Auto exit block will be triggered by enter two new lines
+        if !is_newline(text) {
+            return None;
+        }
+
+        let mut iter = DeltaIter::new(delta);
+        let _prev = iter.next_op_before(index);
+        let _next = iter.next_op();
+
+        None
+    }
+}

+ 3 - 6
rust-lib/flowy-ot/src/client/extensions/insert/auto_format.rs

@@ -1,6 +1,6 @@
 use crate::{
     client::{extensions::InsertExt, util::is_whitespace},
-    core::{CharMetric, Delta, DeltaIter},
+    core::{Delta, DeltaIter},
 };
 
 pub struct AutoFormatExt {}
@@ -50,13 +50,10 @@ impl InsertExt for AutoFormatExt {
     }
 }
 
-use crate::{
-    client::extensions::NEW_LINE,
-    core::{Attribute, AttributeBuilder, Attributes, DeltaBuilder, Operation},
-};
+use crate::core::{Attribute, AttributeBuilder, Attributes, DeltaBuilder, Operation};
 use bytecount::num_chars;
 use std::cmp::min;
-use url::{ParseError, Url};
+use url::Url;
 
 pub enum AutoFormatter {
     Url(Url),

+ 3 - 20
rust-lib/flowy-ot/src/client/extensions/insert/mod.rs

@@ -1,17 +1,16 @@
+pub use auto_exit_block::*;
 pub use auto_format::*;
 pub use default_insert::*;
 pub use preserve_inline_style::*;
 pub use reset_format_on_new_line::*;
 
+mod auto_exit_block;
 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},
-};
+use crate::{client::extensions::InsertExt, core::Delta};
 
 pub struct PreserveBlockStyleOnInsertExt {}
 impl InsertExt for PreserveBlockStyleOnInsertExt {
@@ -43,22 +42,6 @@ impl InsertExt for PreserveLineStyleOnSplitExt {
     }
 }
 
-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<Delta> {
-        None
-    }
-}
-
 pub struct InsertEmbedsExt {}
 impl InsertExt for InsertEmbedsExt {
     fn ext_name(&self) -> &str { "InsertEmbedsExt" }

+ 1 - 1
rust-lib/flowy-ot/src/client/extensions/insert/preserve_inline_style.rs

@@ -3,7 +3,7 @@ use crate::{
         extensions::InsertExt,
         util::{contain_newline, OpNewline},
     },
-    core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter},
+    core::{AttributeKey, Attributes, Delta, DeltaBuilder, DeltaIter},
 };
 
 pub struct PreserveInlineStylesExt {}

+ 1 - 1
rust-lib/flowy-ot/src/client/util.rs

@@ -50,7 +50,7 @@ impl OpNewline {
     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
+        self.is_start() || self.is_end() || self.is_equal() || self == &OpNewline::Contain
     }
 
     pub fn is_equal(&self) -> bool { self == &OpNewline::Equal }

+ 185 - 0
rust-lib/flowy-ot/src/core/attributes/attribute.rs

@@ -0,0 +1,185 @@
+use crate::core::{Attributes, REMOVE_FLAG};
+use derive_more::Display;
+use lazy_static::lazy_static;
+use std::{collections::HashSet, fmt, fmt::Formatter, iter::FromIterator};
+
+lazy_static! {
+    static ref BLOCK_KEYS: HashSet<AttributeKey> = HashSet::from_iter(vec![
+        AttributeKey::Header,
+        AttributeKey::Align,
+        AttributeKey::List,
+        AttributeKey::CodeBlock,
+        AttributeKey::QuoteBlock,
+        AttributeKey::Indent,
+    ]);
+    static ref INLINE_KEYS: HashSet<AttributeKey> = HashSet::from_iter(vec![
+        AttributeKey::Bold,
+        AttributeKey::Italic,
+        AttributeKey::Underline,
+        AttributeKey::StrikeThrough,
+        AttributeKey::Link,
+        AttributeKey::Color,
+        AttributeKey::Background,
+    ]);
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum AttributeScope {
+    Inline,
+    Block,
+    Embeds,
+    Ignore,
+}
+
+#[derive(Debug, Clone)]
+pub struct Attribute {
+    pub key: AttributeKey,
+    pub value: AttributeValue,
+    pub scope: AttributeScope,
+}
+
+impl fmt::Display for Attribute {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        let s = format!("{:?}:{} {:?}", self.key, self.value.as_ref(), self.scope);
+        f.write_str(&s)
+    }
+}
+
+impl std::convert::Into<Attributes> for Attribute {
+    fn into(self) -> Attributes {
+        let mut attributes = Attributes::new();
+        attributes.add(self);
+        attributes
+    }
+}
+
+#[derive(Clone, Debug, Display, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "camelCase")]
+pub enum AttributeKey {
+    #[display(fmt = "bold")]
+    Bold,
+    #[display(fmt = "italic")]
+    Italic,
+    #[display(fmt = "underline")]
+    Underline,
+    #[display(fmt = "strike_through")]
+    StrikeThrough,
+    #[display(fmt = "font")]
+    Font,
+    #[display(fmt = "size")]
+    Size,
+    #[display(fmt = "link")]
+    Link,
+    #[display(fmt = "color")]
+    Color,
+    #[display(fmt = "background")]
+    Background,
+    #[display(fmt = "indent")]
+    Indent,
+    #[display(fmt = "align")]
+    Align,
+    #[display(fmt = "code_block")]
+    CodeBlock,
+    #[display(fmt = "list")]
+    List,
+    #[display(fmt = "quote_block")]
+    QuoteBlock,
+    #[display(fmt = "width")]
+    Width,
+    #[display(fmt = "height")]
+    Height,
+    #[display(fmt = "style")]
+    Style,
+    #[display(fmt = "header")]
+    Header,
+    #[display(fmt = "left")]
+    LeftAlignment,
+    #[display(fmt = "center")]
+    CenterAlignment,
+    #[display(fmt = "right")]
+    RightAlignment,
+    #[display(fmt = "justify")]
+    JustifyAlignment,
+    #[display(fmt = "bullet")]
+    Bullet,
+    #[display(fmt = "ordered")]
+    Ordered,
+    #[display(fmt = "checked")]
+    Checked,
+    #[display(fmt = "unchecked")]
+    UnChecked,
+}
+
+impl AttributeKey {
+    pub fn remove(&self) -> Attribute { self.value(REMOVE_FLAG) }
+
+    pub fn value<T: Into<AttributeValue>>(&self, value: T) -> Attribute {
+        let key = self.clone();
+        let value: AttributeValue = value.into();
+        match self {
+            AttributeKey::Bold
+            | AttributeKey::Italic
+            | AttributeKey::Underline
+            | AttributeKey::StrikeThrough
+            | AttributeKey::Link
+            | AttributeKey::Color
+            | AttributeKey::Background
+            | AttributeKey::Font
+            | AttributeKey::Size => Attribute {
+                key,
+                value,
+                scope: AttributeScope::Inline,
+            },
+
+            AttributeKey::Header
+            | AttributeKey::LeftAlignment
+            | AttributeKey::CenterAlignment
+            | AttributeKey::RightAlignment
+            | AttributeKey::JustifyAlignment
+            | AttributeKey::Indent
+            | AttributeKey::Align
+            | AttributeKey::CodeBlock
+            | AttributeKey::List
+            | AttributeKey::Bullet
+            | AttributeKey::Ordered
+            | AttributeKey::Checked
+            | AttributeKey::UnChecked
+            | AttributeKey::QuoteBlock => Attribute {
+                key,
+                value,
+                scope: AttributeScope::Block,
+            },
+
+            AttributeKey::Width | AttributeKey::Height | AttributeKey::Style => Attribute {
+                key,
+                value,
+                scope: AttributeScope::Ignore,
+            },
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct AttributeValue(pub(crate) String);
+
+impl AsRef<str> for AttributeValue {
+    fn as_ref(&self) -> &str { &self.0 }
+}
+
+impl std::convert::From<&usize> for AttributeValue {
+    fn from(val: &usize) -> Self { AttributeValue(format!("{}", val)) }
+}
+
+impl std::convert::From<&str> for AttributeValue {
+    fn from(val: &str) -> Self { AttributeValue(val.to_owned()) }
+}
+
+impl std::convert::From<bool> for AttributeValue {
+    fn from(val: bool) -> Self {
+        let val = match val {
+            true => "true",
+            false => "",
+        };
+        AttributeValue(val.to_owned())
+    }
+}

+ 1 - 164
rust-lib/flowy-ot/src/core/attributes/builder.rs

@@ -1,100 +1,4 @@
-use crate::core::{Attributes, REMOVE_FLAG};
-use derive_more::Display;
-use std::{fmt, fmt::Formatter};
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct AttributeValue(pub(crate) String);
-
-impl AsRef<str> for AttributeValue {
-    fn as_ref(&self) -> &str { &self.0 }
-}
-
-#[derive(Clone, Debug, Display, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
-#[serde(rename_all = "camelCase")]
-pub enum AttributeKey {
-    #[display(fmt = "bold")]
-    Bold,
-    #[display(fmt = "italic")]
-    Italic,
-    #[display(fmt = "underline")]
-    Underline,
-    #[display(fmt = "strike_through")]
-    StrikeThrough,
-    #[display(fmt = "font")]
-    Font,
-    #[display(fmt = "size")]
-    Size,
-    #[display(fmt = "link")]
-    Link,
-    #[display(fmt = "color")]
-    Color,
-    #[display(fmt = "background")]
-    Background,
-    #[display(fmt = "ident")]
-    Ident,
-    #[display(fmt = "align")]
-    Align,
-    #[display(fmt = "code_block")]
-    CodeBlock,
-    #[display(fmt = "list")]
-    List,
-    #[display(fmt = "quote_block")]
-    QuoteBlock,
-    #[display(fmt = "width")]
-    Width,
-    #[display(fmt = "height")]
-    Height,
-    #[display(fmt = "style")]
-    Style,
-    #[display(fmt = "header")]
-    Header,
-    #[display(fmt = "left")]
-    LeftAlignment,
-    #[display(fmt = "center")]
-    CenterAlignment,
-    #[display(fmt = "right")]
-    RightAlignment,
-    #[display(fmt = "justify")]
-    JustifyAlignment,
-    #[display(fmt = "bullet")]
-    Bullet,
-    #[display(fmt = "ordered")]
-    Ordered,
-    #[display(fmt = "checked")]
-    Checked,
-    #[display(fmt = "unchecked")]
-    UnChecked,
-}
-
-#[derive(Debug, PartialEq, Eq, Clone)]
-pub enum AttributeScope {
-    Inline,
-    Block,
-    Embeds,
-    Ignore,
-}
-
-#[derive(Debug, Clone)]
-pub struct Attribute {
-    pub key: AttributeKey,
-    pub value: AttributeValue,
-    pub scope: AttributeScope,
-}
-
-impl fmt::Display for Attribute {
-    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        let s = format!("{:?}:{} {:?}", self.key, self.value.as_ref(), self.scope);
-        f.write_str(&s)
-    }
-}
-
-impl std::convert::Into<Attributes> for Attribute {
-    fn into(self) -> Attributes {
-        let mut attributes = Attributes::new();
-        attributes.add(self);
-        attributes
-    }
-}
+use crate::core::{Attribute, AttributeKey, AttributeValue, Attributes, REMOVE_FLAG};
 
 pub struct AttributeBuilder {
     inner: Attributes,
@@ -155,70 +59,3 @@ impl AttributeBuilder {
 
     pub fn build(self) -> Attributes { self.inner }
 }
-
-impl AttributeKey {
-    pub fn remove(&self) -> Attribute { self.value(REMOVE_FLAG) }
-
-    pub fn value<T: Into<AttributeValue>>(&self, value: T) -> Attribute {
-        let key = self.clone();
-        let value: AttributeValue = value.into();
-        match self {
-            AttributeKey::Bold
-            | AttributeKey::Italic
-            | AttributeKey::Underline
-            | AttributeKey::StrikeThrough
-            | AttributeKey::Link
-            | AttributeKey::Color
-            | AttributeKey::Background
-            | AttributeKey::Font
-            | AttributeKey::Size => Attribute {
-                key,
-                value,
-                scope: AttributeScope::Inline,
-            },
-
-            AttributeKey::Header
-            | AttributeKey::LeftAlignment
-            | AttributeKey::CenterAlignment
-            | AttributeKey::RightAlignment
-            | AttributeKey::JustifyAlignment
-            | AttributeKey::Ident
-            | AttributeKey::Align
-            | AttributeKey::CodeBlock
-            | AttributeKey::List
-            | AttributeKey::Bullet
-            | AttributeKey::Ordered
-            | AttributeKey::Checked
-            | AttributeKey::UnChecked
-            | AttributeKey::QuoteBlock => Attribute {
-                key,
-                value,
-                scope: AttributeScope::Block,
-            },
-
-            AttributeKey::Width | AttributeKey::Height | AttributeKey::Style => Attribute {
-                key,
-                value,
-                scope: AttributeScope::Ignore,
-            },
-        }
-    }
-}
-
-impl std::convert::From<&usize> for AttributeValue {
-    fn from(val: &usize) -> Self { AttributeValue(format!("{}", val)) }
-}
-
-impl std::convert::From<&str> for AttributeValue {
-    fn from(val: &str) -> Self { AttributeValue(val.to_owned()) }
-}
-
-impl std::convert::From<bool> for AttributeValue {
-    fn from(val: bool) -> Self {
-        let val = match val {
-            true => "true",
-            false => "",
-        };
-        AttributeValue(val.to_owned())
-    }
-}

+ 2 - 0
rust-lib/flowy-ot/src/core/attributes/mod.rs

@@ -1,6 +1,8 @@
+mod attribute;
 mod attributes;
 mod attributes_serde;
 mod builder;
 
+pub use attribute::*;
 pub use attributes::*;
 pub use builder::*;

+ 1 - 1
rust-lib/flowy-ot/src/core/delta/delta.rs

@@ -625,7 +625,7 @@ fn invert_from_other(
             );
             let inverted_attrs =
                 invert_attributes(operation.get_attributes(), other_op.get_attributes());
-            log::debug!("invert result: {:?}", inverted_attrs);
+            log::debug!("invert attributes result: {:?}", inverted_attrs);
             log::debug!(
                 "invert retain: {} by retain len: {}, {}",
                 retain,

+ 37 - 0
rust-lib/flowy-ot/tests/attribute_test.rs

@@ -635,3 +635,40 @@ fn attributes_auto_format_exist_link2() {
 
     OpTester::new().run_script_with_newline(ops);
 }
+
+#[test]
+fn attributes_add_bullet() {
+    let ops = vec![
+        Insert(0, "1", 0),
+        Bullet(0, Interval::new(0, 1), true),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+        Insert(0, NEW_LINE, 1),
+        Insert(0, "2", 2),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"2"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}
+
+#[test]
+fn attributes_un_bullet_one() {
+    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),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"2\n"}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}

+ 11 - 0
rust-lib/flowy-ot/tests/helper/mod.rs

@@ -33,6 +33,9 @@ pub enum TestOp {
     #[display(fmt = "Link")]
     Link(usize, Interval, &'static str, bool),
 
+    #[display(fmt = "Bullet")]
+    Bullet(usize, Interval, bool),
+
     #[display(fmt = "Transform")]
     Transform(usize, usize),
 
@@ -126,6 +129,14 @@ impl OpTester {
                 };
                 document.format(*iv, attribute).unwrap();
             },
+            TestOp::Bullet(delta_i, iv, enable) => {
+                let document = &mut self.documents[*delta_i];
+                let attribute = match *enable {
+                    true => AttributeKey::Bullet.value("true"),
+                    false => AttributeKey::Bullet.remove(),
+                };
+                document.format(*iv, attribute).unwrap();
+            },
             TestOp::Transform(delta_a_i, delta_b_i) => {
                 let (a_prime, b_prime) = self.documents[*delta_a_i]
                     .data()

+ 19 - 0
rust-lib/flowy-ot/tests/undo_redo_test.rs

@@ -267,3 +267,22 @@ fn history_undo_add_header() {
 
     OpTester::new().run_script_with_newline(ops);
 }
+
+#[test]
+fn history_undo_add_link() {
+    let site = "https://appflowy.io";
+    let ops = vec![
+        Insert(0, site, 0),
+        Wait(RECORD_THRESHOLD),
+        Link(0, Interval::new(0, site.len()), site, true),
+        Undo(0),
+        AssertOpsJson(0, r#"[{"insert":"https://appflowy.io\n"}]"#),
+        Redo(0),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"https://appflowy.io","attributes":{"link":"https://appflowy.io"}},{"insert":"\n"}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}