appflowy пре 4 година
родитељ
комит
4ab4f744ba

+ 1 - 0
app_flowy/packages/flowy_editor/lib/src/model/document/document.dart

@@ -73,6 +73,7 @@ class Document {
     print('insert delta: $delta');
     compose(delta, ChangeSource.LOCAL);
     print('compose insert, current document $_delta');
+    print('compose end');
     return delta;
   }
 

+ 2 - 4
rust-lib/flowy-ot/src/client/extensions/format/format_at_position.rs

@@ -13,10 +13,8 @@ impl FormatExt for FormatLinkAtCaretPositionExt {
             return None;
         }
 
-        let mut iter = DeltaIter::new(delta);
-        iter.seek::<CharMetric>(interval.start);
-
-        let (before, after) = (iter.next_op_before(interval.size()), iter.next());
+        let mut iter = DeltaIter::from_offset(delta, interval.start);
+        let (before, after) = (iter.last_op_before_index(interval.size()), iter.next_op());
         let mut start = interval.end;
         let mut retain = 0;
 

+ 2 - 3
rust-lib/flowy-ot/src/client/extensions/format/resolve_block_format.rs

@@ -25,12 +25,11 @@ impl FormatExt for ResolveBlockFormatExt {
         }
 
         let mut new_delta = DeltaBuilder::new().retain(interval.start).build();
-        let mut iter = DeltaIter::new(delta);
-        iter.seek::<CharMetric>(interval.start);
+        let mut iter = DeltaIter::from_offset(delta, 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();
+            let next_op = iter.last_op_before_index(end - start).unwrap();
             match find_newline(next_op.get_data()) {
                 None => new_delta.retain(next_op.len(), Attributes::empty()),
                 Some(_) => {

+ 2 - 4
rust-lib/flowy-ot/src/client/extensions/format/resolve_inline_format.rs

@@ -15,14 +15,12 @@ impl FormatExt for ResolveInlineFormatExt {
             return None;
         }
         let mut new_delta = DeltaBuilder::new().retain(interval.start).build();
-        let mut iter = DeltaIter::new(delta);
-        iter.seek::<CharMetric>(interval.start);
-
+        let mut iter = DeltaIter::from_offset(delta, 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();
+            let next_op = iter.last_op_before_index(end - start).unwrap();
             match find_newline(next_op.get_data()) {
                 None => new_delta.retain(next_op.len(), attribute.clone().into()),
                 Some(_) => {

+ 53 - 6
rust-lib/flowy-ot/src/client/extensions/insert/auto_exit_block.rs

@@ -1,23 +1,70 @@
 use crate::{
     client::{extensions::InsertExt, util::is_newline},
-    core::{Delta, DeltaIter},
+    core::{
+        AttributeKey,
+        AttributeValue,
+        Attributes,
+        CharMetric,
+        Delta,
+        DeltaBuilder,
+        DeltaIter,
+        Operation,
+    },
 };
 
+use crate::core::is_empty_line_at_index;
+
 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> {
+    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();
+        if !is_empty_line_at_index(delta, index) {
+            return None;
+        }
+
+        let mut iter = DeltaIter::from_offset(delta, index);
+        let next = iter.next_op()?;
+        let mut attributes = next.get_attributes();
 
-        None
+        let block_attributes = attributes_except_header(&next);
+        if block_attributes.is_empty() {
+            return None;
+        }
+
+        if next.len() > 1 {
+            return None;
+        }
+
+        match iter.first_op_contains_newline() {
+            None => {},
+            Some((newline_op, _)) => {
+                let newline_attributes = attributes_except_header(&newline_op);
+                if block_attributes == newline_attributes {
+                    return None;
+                }
+            },
+        }
+
+        attributes.mark_as_removed_except(&AttributeKey::Header);
+
+        Some(
+            DeltaBuilder::new()
+                .retain(index + replace_len)
+                .retain_with_attributes(1, attributes)
+                .build(),
+        )
     }
 }
+
+fn attributes_except_header(op: &Operation) -> Attributes {
+    let mut attributes = op.get_attributes();
+    attributes.remove(AttributeKey::Header);
+    attributes
+}

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

@@ -13,7 +13,7 @@ impl InsertExt for AutoFormatExt {
             return None;
         }
         let mut iter = DeltaIter::new(delta);
-        if let Some(prev) = iter.next_op_before(index) {
+        if let Some(prev) = iter.last_op_before_index(index) {
             match AutoFormat::parse(prev.get_data()) {
                 None => {},
                 Some(formatter) => {

+ 2 - 2
rust-lib/flowy-ot/src/client/extensions/insert/default_insert.rs

@@ -1,6 +1,6 @@
 use crate::{
-    client::extensions::{InsertExt, NEW_LINE},
-    core::{AttributeKey, Attributes, Delta, DeltaBuilder, DeltaIter},
+    client::extensions::InsertExt,
+    core::{AttributeKey, Attributes, Delta, DeltaBuilder, DeltaIter, NEW_LINE},
 };
 
 pub struct DefaultInsertExt {}

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

@@ -1,9 +1,6 @@
 use crate::{
-    client::{
-        extensions::InsertExt,
-        util::{contain_newline, OpNewline},
-    },
-    core::{AttributeKey, Attributes, Delta, DeltaBuilder, DeltaIter},
+    client::{extensions::InsertExt, util::contain_newline},
+    core::{AttributeKey, Attributes, Delta, DeltaBuilder, DeltaIter, OpNewline},
 };
 
 pub struct PreserveInlineStylesExt {}
@@ -16,7 +13,7 @@ impl InsertExt for PreserveInlineStylesExt {
         }
 
         let mut iter = DeltaIter::new(delta);
-        let prev = iter.next_op_before(index)?;
+        let prev = iter.last_op_before_index(index)?;
         if OpNewline::parse(&prev).is_contain() {
             return None;
         }

+ 3 - 7
rust-lib/flowy-ot/src/client/extensions/insert/reset_format_on_new_line.rs

@@ -1,9 +1,6 @@
 use crate::{
-    client::{
-        extensions::{InsertExt, NEW_LINE},
-        util::is_newline,
-    },
-    core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter},
+    client::{extensions::InsertExt, util::is_newline},
+    core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter, NEW_LINE},
 };
 
 pub struct ResetLineFormatOnNewLineExt {}
@@ -16,8 +13,7 @@ impl InsertExt for ResetLineFormatOnNewLineExt {
         }
 
         let mut iter = DeltaIter::new(delta);
-        iter.seek::<CharMetric>(index);
-        let next_op = iter.next()?;
+        let next_op = iter.first_op_after_index(index)?;
         if !next_op.get_data().starts_with(NEW_LINE) {
             return None;
         }

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

@@ -8,9 +8,6 @@ mod delete;
 mod format;
 mod insert;
 
-pub const NEW_LINE: &'static str = "\n";
-pub const WHITESPACE: &'static str = " ";
-
 pub type InsertExtension = Box<dyn InsertExt>;
 pub type FormatExtension = Box<dyn FormatExt>;
 pub type DeleteExtension = Box<dyn DeleteExt>;

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

@@ -1,7 +1,4 @@
-use crate::{
-    client::extensions::{NEW_LINE, WHITESPACE},
-    core::Operation,
-};
+use crate::core::{Operation, NEW_LINE, WHITESPACE};
 
 #[inline]
 pub fn find_newline(s: &str) -> Option<usize> {
@@ -11,51 +8,6 @@ pub fn find_newline(s: &str) -> Option<usize> {
     }
 }
 
-#[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.is_equal() || 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()) }
 

+ 37 - 34
rust-lib/flowy-ot/src/core/attributes/attribute.rs

@@ -6,11 +6,19 @@ use std::{collections::HashSet, fmt, fmt::Formatter, iter::FromIterator};
 lazy_static! {
     static ref BLOCK_KEYS: HashSet<AttributeKey> = HashSet::from_iter(vec![
         AttributeKey::Header,
+        AttributeKey::LeftAlignment,
+        AttributeKey::CenterAlignment,
+        AttributeKey::RightAlignment,
+        AttributeKey::JustifyAlignment,
+        AttributeKey::Indent,
         AttributeKey::Align,
-        AttributeKey::List,
         AttributeKey::CodeBlock,
+        AttributeKey::List,
+        AttributeKey::Bullet,
+        AttributeKey::Ordered,
+        AttributeKey::Checked,
+        AttributeKey::UnChecked,
         AttributeKey::QuoteBlock,
-        AttributeKey::Indent,
     ]);
     static ref INLINE_KEYS: HashSet<AttributeKey> = HashSet::from_iter(vec![
         AttributeKey::Bold,
@@ -19,8 +27,15 @@ lazy_static! {
         AttributeKey::StrikeThrough,
         AttributeKey::Link,
         AttributeKey::Color,
+        AttributeKey::Font,
+        AttributeKey::Size,
         AttributeKey::Background,
     ]);
+    static ref INGORE_KEYS: HashSet<AttributeKey> = HashSet::from_iter(vec![
+        AttributeKey::Width,
+        AttributeKey::Height,
+        AttributeKey::Style,
+    ]);
 }
 
 #[derive(Debug, PartialEq, Eq, Clone)]
@@ -116,45 +131,26 @@ impl AttributeKey {
     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 {
+        if INLINE_KEYS.contains(self) {
+            return 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 {
+            };
+        }
+
+        if BLOCK_KEYS.contains(self) {
+            return Attribute {
                 key,
                 value,
                 scope: AttributeScope::Block,
-            },
+            };
+        }
 
-            AttributeKey::Width | AttributeKey::Height | AttributeKey::Style => Attribute {
-                key,
-                value,
-                scope: AttributeScope::Ignore,
-            },
+        Attribute {
+            key,
+            value,
+            scope: AttributeScope::Ignore,
         }
     }
 }
@@ -183,3 +179,10 @@ impl std::convert::From<bool> for AttributeValue {
         AttributeValue(val.to_owned())
     }
 }
+
+pub fn is_block_except_header(k: &AttributeKey) -> bool {
+    if k == &AttributeKey::Header {
+        return false;
+    }
+    BLOCK_KEYS.contains(k)
+}

+ 27 - 17
rust-lib/flowy-ot/src/core/attributes/attributes.rs

@@ -37,17 +37,39 @@ impl Attributes {
         self.inner.insert(key, value);
     }
 
-    pub fn remove(&mut self, key: &AttributeKey) {
+    pub fn mark_as_removed(&mut self, key: &AttributeKey) {
         let value: AttributeValue = REMOVE_FLAG.into();
         self.inner.insert(key.clone(), value);
     }
 
-    // Remove the key if its value is empty. e.g. { bold: "" }
-    pub fn remove_empty_value(&mut self) { self.inner.retain(|_, v| !should_remove(v)); }
+    pub fn mark_as_removed_except(&mut self, attribute: &AttributeKey) {
+        self.inner.iter_mut().for_each(|(k, v)| {
+            if k != attribute {
+                v.0 = REMOVE_FLAG.into();
+            }
+            v.0 = REMOVE_FLAG.into();
+        });
+    }
+
+    pub fn remove(&mut self, key: AttributeKey) { self.inner.retain(|k, _| k != &key); }
+
+    // pub fn block_attributes_except_header(attributes: &Attributes) -> Attributes
+    // {     let mut new_attributes = Attributes::new();
+    //     attributes.iter().for_each(|(k, v)| {
+    //         if k != &AttributeKey::Header {
+    //             new_attributes.insert(k.clone(), v.clone());
+    //         }
+    //     });
+    //
+    //     new_attributes
+    // }
+
+    // Remove the empty attribute which value is empty. e.g. {bold: ""}.
+    pub fn remove_empty(&mut self) { self.inner.retain(|_, v| !should_remove(v)); }
 
     pub fn extend(&mut self, other: Attributes) { self.inner.extend(other.inner); }
 
-    // Update self attributes by constructing new attributes from the other if it's
+    // Update inner by constructing new attributes from the other if it's
     // not None and replace the key/value with self key/value.
     pub fn merge(&mut self, other: Option<Attributes>) {
         if other.is_none() {
@@ -144,7 +166,7 @@ pub fn invert_attributes(attr: Attributes, base: Attributes) -> Attributes {
 
     let inverted = attr.iter().fold(base_inverted, |mut attributes, (k, _)| {
         if base.get(k) != attr.get(k) && !base.contains_key(k) {
-            attributes.remove(k);
+            attributes.mark_as_removed(k);
         }
         attributes
     });
@@ -156,15 +178,3 @@ pub fn merge_attributes(mut attributes: Attributes, other: Attributes) -> Attrib
     attributes.extend(other);
     attributes
 }
-
-pub trait AttributesRule {
-    // Remove the empty attribute that its value is empty. e.g. {bold: ""}.
-    fn remove_empty(self) -> Attributes;
-}
-
-impl AttributesRule for Attributes {
-    fn remove_empty(mut self) -> Attributes {
-        self.remove_empty_value();
-        self.into()
-    }
-}

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

@@ -34,10 +34,10 @@ impl<'a> Cursor<'a> {
     // get the next operation interval
     pub fn next_iv(&self) -> Interval { self.next_iv_before(None) }
 
-    pub fn next_op(&mut self) -> Option<Operation> { self.next_op_before(None) }
+    pub fn next_op(&mut self) -> Option<Operation> { self.last_op_before_index(None) }
 
     // get the last operation before the index
-    pub fn next_op_before(&mut self, index: Option<usize>) -> Option<Operation> {
+    pub fn last_op_before_index(&mut self, index: Option<usize>) -> Option<Operation> {
         let mut find_op = None;
         let next_op = self.next_op.take();
         let mut next_op = next_op.as_ref();
@@ -74,7 +74,7 @@ impl<'a> Cursor<'a> {
             let pos = self.cur_char_count - pre_char_count;
             let end = index.unwrap();
             if end > pos {
-                return self.next_op_before(Some(end - pos));
+                return self.last_op_before_index(Some(end - pos));
             }
         }
         return find_op;
@@ -173,7 +173,7 @@ pub struct CharMetric {}
 impl Metric for CharMetric {
     fn seek(cursor: &mut Cursor, index: usize) -> SeekResult {
         let _ = check_bound(cursor.cur_char_count, index)?;
-        let _ = cursor.next_op_before(Some(index));
+        let _ = cursor.last_op_before_index(Some(index));
 
         Ok(())
     }

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

@@ -168,11 +168,11 @@ impl Delta {
             );
 
             let op = iter
-                .next_op_before(length)
+                .last_op_before_index(length)
                 .unwrap_or(OpBuilder::retain(length).build());
 
             let other_op = other_iter
-                .next_op_before(length)
+                .last_op_before_index(length)
                 .unwrap_or(OpBuilder::retain(length).build());
 
             debug_assert_eq!(op.len(), other_op.len());
@@ -194,7 +194,7 @@ impl Delta {
                         insert.attributes.clone(),
                         other_retain.attributes.clone(),
                     );
-                    composed_attrs = composed_attrs.remove_empty();
+                    composed_attrs.remove_empty();
                     new_delta.add(
                         OpBuilder::insert(op.get_data())
                             .attributes(composed_attrs)
@@ -300,7 +300,7 @@ impl Delta {
                 },
                 (Some(Operation::Insert(insert)), Some(Operation::Retain(o_retain))) => {
                     let mut composed_attrs = compose_operation(&next_op1, &next_op2);
-                    composed_attrs = composed_attrs.remove_empty();
+                    composed_attrs.remove_empty();
 
                     log::debug!(
                         "compose: [{} - {}], composed_attrs: {}",

+ 93 - 5
rust-lib/flowy-ot/src/core/delta/iterator.rs

@@ -1,5 +1,5 @@
 use super::cursor::*;
-use crate::core::{Attributes, Delta, Interval, Operation};
+use crate::core::{Attributes, Delta, Interval, Operation, NEW_LINE};
 use std::ops::{Deref, DerefMut};
 
 pub(crate) const MAX_IV_LEN: usize = i32::MAX as usize;
@@ -14,6 +14,13 @@ impl<'a> DeltaIter<'a> {
         Self::from_interval(delta, interval)
     }
 
+    pub fn from_offset(delta: &'a Delta, offset: usize) -> Self {
+        let interval = Interval::new(0, MAX_IV_LEN);
+        let mut iter = Self::from_interval(delta, interval);
+        iter.seek::<CharMetric>(offset);
+        iter
+    }
+
     pub fn from_interval(delta: &'a Delta, interval: Interval) -> Self {
         let cursor = Cursor::new(delta, interval);
         Self { cursor }
@@ -21,8 +28,6 @@ impl<'a> DeltaIter<'a> {
 
     pub fn ops(&mut self) -> Vec<Operation> { self.collect::<Vec<_>>() }
 
-    pub fn next_op(&mut self) -> Option<Operation> { self.cursor.next_op() }
-
     pub fn next_op_len(&self) -> Option<usize> {
         let interval = self.cursor.next_iv();
         if interval.is_empty() {
@@ -32,8 +37,30 @@ impl<'a> DeltaIter<'a> {
         }
     }
 
-    pub fn next_op_before(&mut self, index: usize) -> Option<Operation> {
-        self.cursor.next_op_before(Some(index))
+    // find next op contains NEW_LINE
+    pub fn first_op_contains_newline(&mut self) -> Option<(Operation, usize)> {
+        let mut offset = 0;
+        while self.has_next() {
+            if let Some(op) = self.next_op() {
+                if OpNewline::parse(&op).is_contain() {
+                    return Some((op, offset));
+                }
+                offset += op.len();
+            }
+        }
+
+        None
+    }
+
+    pub fn next_op(&mut self) -> Option<Operation> { self.cursor.next_op() }
+
+    pub fn last_op_before_index(&mut self, index: usize) -> Option<Operation> {
+        self.cursor.last_op_before_index(Some(index))
+    }
+
+    pub fn first_op_after_index(&mut self, index: usize) -> Option<Operation> {
+        self.seek::<CharMetric>(index);
+        self.next_op()
     }
 
     pub fn seek<M: Metric>(&mut self, index: usize) {
@@ -72,6 +99,22 @@ impl<'a> Iterator for DeltaIter<'a> {
     fn next(&mut self) -> Option<Self::Item> { self.next_op() }
 }
 
+pub fn is_empty_line_at_index(delta: &Delta, index: usize) -> bool {
+    let mut iter = DeltaIter::new(delta);
+    let (prev, next) = (iter.last_op_before_index(index), iter.next_op());
+    if prev.is_none() {
+        return true;
+    }
+
+    if next.is_none() {
+        return false;
+    }
+
+    let prev = prev.unwrap();
+    let next = next.unwrap();
+    OpNewline::parse(&prev).is_end() && OpNewline::parse(&next).is_start()
+}
+
 pub struct AttributesIter<'a> {
     delta_iter: DeltaIter<'a>,
 }
@@ -133,3 +176,48 @@ impl<'a> Iterator for AttributesIter<'a> {
         Some((length, attributes))
     }
 }
+
+#[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 || self.is_equal() }
+
+    pub fn is_end(&self) -> bool { self == &OpNewline::End || self.is_equal() }
+
+    pub fn is_not_found(&self) -> bool { self == &OpNewline::NotFound }
+
+    pub fn is_contain(&self) -> bool {
+        self.is_start() || self.is_end() || self.is_equal() || self == &OpNewline::Contain
+    }
+
+    pub fn is_equal(&self) -> bool { self == &OpNewline::Equal }
+}

+ 3 - 0
rust-lib/flowy-ot/src/core/delta/mod.rs

@@ -8,3 +8,6 @@ pub use builder::*;
 pub use cursor::*;
 pub use delta::*;
 pub use iterator::*;
+
+pub const NEW_LINE: &'static str = "\n";
+pub const WHITESPACE: &'static str = " ";

+ 21 - 1
rust-lib/flowy-ot/tests/attribute_test.rs

@@ -3,7 +3,7 @@ pub mod helper;
 use crate::helper::{TestOp::*, *};
 use flowy_ot::core::Interval;
 
-use flowy_ot::client::extensions::{NEW_LINE, WHITESPACE};
+use flowy_ot::core::{NEW_LINE, WHITESPACE};
 
 #[test]
 fn attributes_insert_text() {
@@ -646,6 +646,10 @@ fn attributes_add_bullet() {
             r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}}]"#,
         ),
         Insert(0, NEW_LINE, 1),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n\n","attributes":{"bullet":"true"}}]"#,
+        ),
         Insert(0, "2", 2),
         AssertOpsJson(
             0,
@@ -672,3 +676,19 @@ fn attributes_un_bullet_one() {
 
     OpTester::new().run_script_with_newline(ops);
 }
+
+#[test]
+fn attributes_auto_exit_block() {
+    let ops = vec![
+        Insert(0, "1", 0),
+        Bullet(0, Interval::new(0, 1), true),
+        Insert(0, NEW_LINE, 1),
+        Insert(0, NEW_LINE, 2),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"1"},{"insert":"\n","attributes":{"bullet":"true"}},{"insert":"\n"}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}

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

@@ -3,7 +3,7 @@ use flowy_ot::{client::Document, core::*};
 use rand::{prelude::*, Rng as WrappedRng};
 use std::{sync::Once, time::Duration};
 
-const LEVEL: &'static str = "info";
+const LEVEL: &'static str = "debug";
 
 #[derive(Clone, Debug, Display)]
 pub enum TestOp {

+ 21 - 12
rust-lib/flowy-ot/tests/op_test.rs

@@ -147,14 +147,13 @@ fn delta_get_ops_in_interval_7() {
     delta.add(insert_a.clone());
     delta.add(retain_a.clone());
 
-    let mut iter_1 = DeltaIter::new(&delta);
-    iter_1.seek::<CharMetric>(2);
+    let mut iter_1 = DeltaIter::from_offset(&delta, 2);
     assert_eq!(iter_1.next_op().unwrap(), OpBuilder::insert("345").build());
     assert_eq!(iter_1.next_op().unwrap(), OpBuilder::retain(3).build());
 
     let mut iter_2 = DeltaIter::new(&delta);
     assert_eq!(
-        iter_2.next_op_before(2).unwrap(),
+        iter_2.last_op_before_index(2).unwrap(),
         OpBuilder::insert("12").build()
     );
     assert_eq!(iter_2.next_op().unwrap(), OpBuilder::insert("345").build());
@@ -181,7 +180,7 @@ fn delta_seek_2() {
 
     let mut iter = DeltaIter::new(&delta);
     assert_eq!(
-        iter.next_op_before(1).unwrap(),
+        iter.last_op_before_index(1).unwrap(),
         OpBuilder::insert("1").build()
     );
 }
@@ -193,21 +192,21 @@ fn delta_seek_3() {
 
     let mut iter = DeltaIter::new(&delta);
     assert_eq!(
-        iter.next_op_before(2).unwrap(),
+        iter.last_op_before_index(2).unwrap(),
         OpBuilder::insert("12").build()
     );
 
     assert_eq!(
-        iter.next_op_before(2).unwrap(),
+        iter.last_op_before_index(2).unwrap(),
         OpBuilder::insert("34").build()
     );
 
     assert_eq!(
-        iter.next_op_before(2).unwrap(),
+        iter.last_op_before_index(2).unwrap(),
         OpBuilder::insert("5").build()
     );
 
-    assert_eq!(iter.next_op_before(1), None);
+    assert_eq!(iter.last_op_before_index(1), None);
 }
 
 #[test]
@@ -218,7 +217,7 @@ fn delta_seek_4() {
     let mut iter = DeltaIter::new(&delta);
     iter.seek::<CharMetric>(3);
     assert_eq!(
-        iter.next_op_before(2).unwrap(),
+        iter.last_op_before_index(2).unwrap(),
         OpBuilder::insert("45").build()
     );
 }
@@ -238,7 +237,7 @@ fn delta_seek_5() {
     iter.seek::<CharMetric>(0);
 
     assert_eq!(
-        iter.next_op_before(4).unwrap(),
+        iter.last_op_before_index(4).unwrap(),
         OpBuilder::insert("1234").attributes(attributes).build(),
     );
 }
@@ -252,7 +251,7 @@ fn delta_next_op_len_test() {
     iter.seek::<CharMetric>(3);
     assert_eq!(iter.next_op_len().unwrap(), 2);
     assert_eq!(
-        iter.next_op_before(1).unwrap(),
+        iter.last_op_before_index(1).unwrap(),
         OpBuilder::insert("4").build()
     );
     assert_eq!(iter.next_op_len().unwrap(), 1);
@@ -267,12 +266,22 @@ fn delta_next_op_len_test2() {
 
     assert_eq!(iter.next_op_len().unwrap(), 5);
     assert_eq!(
-        iter.next_op_before(5).unwrap(),
+        iter.last_op_before_index(5).unwrap(),
         OpBuilder::insert("12345").build()
     );
     assert_eq!(iter.next_op_len(), None);
 }
 
+#[test]
+fn delta_next_op_len_test3() {
+    let mut delta = Delta::default();
+    delta.add(OpBuilder::insert("12345").build());
+    let mut iter = DeltaIter::new(&delta);
+
+    assert_eq!(iter.last_op_before_index(0), None,);
+    assert_eq!(iter.next_op_len().unwrap(), 5);
+}
+
 #[test]
 fn lengths() {
     let mut delta = Delta::default();