소스 검색

config header attribute & add test

appflowy 3 년 전
부모
커밋
aef5e54c3f

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

@@ -52,6 +52,7 @@ class Document {
   bool get hasRedo => _history.hasRedo;
 
   Delta insert(int index, Object? data, {int replaceLength = 0}) {
+    print('insert $data at $index');
     assert(index >= 0);
     assert(data is String || data is Embeddable);
     if (data is Embeddable) {
@@ -67,7 +68,10 @@ class Document {
       data: data,
       length: replaceLength,
     );
+
+    print('insert delta: $delta');
     compose(delta, ChangeSource.LOCAL);
+    print('compose insert, current document $_delta');
     return delta;
   }
 
@@ -76,6 +80,7 @@ class Document {
     final delta = _rules.apply(RuleType.DELETE, this, index, length: length);
     if (delta.isNotEmpty) {
       compose(delta, ChangeSource.LOCAL);
+      print('compose delete, current document $_delta');
     }
     return delta;
   }
@@ -92,14 +97,17 @@ class Document {
     // We have to insert before applying delete rules
     // Otherwise delete would be operating on stale document snapshot.
     if (dataIsNotEmpty) {
+      print('insert $data at $index, replace len: $length');
       delta = insert(index, data, replaceLength: length);
     }
 
     if (length > 0) {
+      print('delete $length at $index, len: $length');
       final deleteDelta = delete(index, length);
       delta = delta.compose(deleteDelta);
     }
 
+    print('replace result $delta');
     return delta;
   }
 
@@ -117,6 +125,7 @@ class Document {
     );
     if (formatDelta.isNotEmpty) {
       compose(formatDelta, ChangeSource.LOCAL);
+      print('compose format, current document $_delta');
       delta = delta.compose(formatDelta);
     }
 

+ 12 - 4
app_flowy/packages/flowy_editor/lib/src/model/heuristic/format.dart

@@ -44,7 +44,9 @@ class ResolveLineFormatRule extends FormatRule {
       for (var lineBreak = text.indexOf('\n');
           lineBreak >= 0;
           lineBreak = text.indexOf('\n', offset)) {
-        tmp..retain(lineBreak - offset)..retain(1, attribute.toJson());
+        tmp
+          ..retain(lineBreak - offset)
+          ..retain(1, attribute.toJson());
         offset = lineBreak + 1;
       }
       tmp.retain(text.length - offset);
@@ -59,7 +61,9 @@ class ResolveLineFormatRule extends FormatRule {
         delta.retain(op.length!);
         continue;
       }
-      delta..retain(lineBreak)..retain(1, attribute.toJson());
+      delta
+        ..retain(lineBreak)
+        ..retain(1, attribute.toJson());
       break;
     }
     return delta;
@@ -91,7 +95,9 @@ class FormatLinkAtCaretPositionRule extends FormatRule {
       return null;
     }
 
-    delta..retain(beg)..retain(retain!, attribute.toJson());
+    delta
+      ..retain(beg)
+      ..retain(retain!, attribute.toJson());
     return delta;
   }
 }
@@ -120,7 +126,9 @@ class ResolveInlineFormatRule extends FormatRule {
       }
       var pos = 0;
       while (lineBreak >= 0) {
-        delta..retain(lineBreak - pos, attribute.toJson())..retain(1);
+        delta
+          ..retain(lineBreak - pos, attribute.toJson())
+          ..retain(1);
         pos = lineBreak + 1;
         lineBreak = text.indexOf('\n', pos);
       }

+ 9 - 3
app_flowy/packages/flowy_editor/lib/src/model/heuristic/insert.dart

@@ -159,7 +159,9 @@ class AutoExitBlockRule extends InsertRule {
         .firstWhere((k) => Attribute.blockKeysExceptHeader.contains(k));
     attributes[k] = null;
     // retain(1) should be '\n', set it with no attribute
-    return Delta()..retain(index + (length ?? 0))..retain(1, attributes);
+    return Delta()
+      ..retain(index + (length ?? 0))
+      ..retain(1, attributes);
   }
 }
 
@@ -261,10 +263,14 @@ class ForceNewlineForInsertsAroundEmbedRule extends InsertRule {
     }
     final delta = Delta()..retain(index + (length ?? 0));
     if (cursorBeforeEmbed && !text.endsWith('\n')) {
-      return delta..insert(text)..insert('\n');
+      return delta
+        ..insert(text)
+        ..insert('\n');
     }
     if (cursorAfterEmbed && !text.startsWith('\n')) {
-      return delta..insert('\n')..insert(text);
+      return delta
+        ..insert('\n')
+        ..insert(text);
     }
     return delta..insert(text);
   }

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

@@ -41,7 +41,7 @@ class Rules {
 
   static final Rules _instance = Rules([
     const FormatLinkAtCaretPositionRule(),
-    // const ResolveLineFormatRule(),
+    const ResolveLineFormatRule(),
     const ResolveInlineFormatRule(),
     // const InsertEmbedsRule(),
     // const ForceNewlineForInsertsAroundEmbedRule(),
@@ -70,6 +70,7 @@ class Rules {
         final result = rule.apply(delta, index,
             length: length, data: data, attribute: attribute);
         if (result != null) {
+          print('apply rule: $rule, result: $result');
           return result..trim();
         }
       } catch (e) {

+ 5 - 2
rust-lib/flowy-ot/src/client/document.rs

@@ -34,6 +34,7 @@ impl Document {
         let interval = Interval::new(index, index);
         let _ = validate_interval(&self.delta, &interval)?;
         let delta = self.view.insert(&self.delta, text, interval)?;
+        log::debug!("👉 receive change: {}", delta);
         self.add_delta(&delta)?;
         Ok(delta)
     }
@@ -43,6 +44,7 @@ impl Document {
         debug_assert_eq!(interval.is_empty(), false);
         let delete = self.view.delete(&self.delta, interval)?;
         if !delete.is_empty() {
+            log::debug!("👉 receive change: {}", delete);
             let _ = self.add_delta(&delete)?;
         }
         Ok(delete)
@@ -56,6 +58,7 @@ impl Document {
             .format(&self.delta, attribute.clone(), interval)
             .unwrap();
 
+        log::debug!("👉 receive change: {}", format_delta);
         self.add_delta(&format_delta)?;
         Ok(())
     }
@@ -65,6 +68,7 @@ impl Document {
         let mut delta = Delta::default();
         if !text.is_empty() {
             delta = self.view.insert(&self.delta, text, interval)?;
+            log::debug!("👉 receive change: {}", delta);
             self.add_delta(&delta)?;
         }
 
@@ -121,7 +125,6 @@ impl Document {
 
 impl Document {
     fn add_delta(&mut self, delta: &Delta) -> Result<(), OTError> {
-        log::debug!("👉invert change {}", delta);
         let composed_delta = self.delta.compose(delta)?;
         let mut undo_delta = delta.invert(&self.delta);
         self.rev_id_counter += 1;
@@ -138,7 +141,7 @@ impl Document {
             self.last_edit_time = now;
         }
 
-        log::debug!("compose previous result: {}", undo_delta);
+        log::debug!("👉 receive change undo: {}", undo_delta);
         if !undo_delta.is_empty() {
             self.history.record(undo_delta);
         }

+ 2 - 0
rust-lib/flowy-ot/src/client/view/delete_ext.rs

@@ -5,6 +5,8 @@ use crate::{
 
 pub struct DefaultDeleteExt {}
 impl DeleteExt for DefaultDeleteExt {
+    fn ext_name(&self) -> &str { "DeleteExt" }
+
     fn apply(&self, _delta: &Delta, interval: Interval) -> Option<Delta> {
         Some(
             DeltaBuilder::new()

+ 3 - 0
rust-lib/flowy-ot/src/client/view/extension.rs

@@ -5,13 +5,16 @@ pub type FormatExtension = Box<dyn FormatExt>;
 pub type DeleteExtension = Box<dyn DeleteExt>;
 
 pub trait InsertExt {
+    fn ext_name(&self) -> &str;
     fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option<Delta>;
 }
 
 pub trait FormatExt {
+    fn ext_name(&self) -> &str;
     fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option<Delta>;
 }
 
 pub trait DeleteExt {
+    fn ext_name(&self) -> &str;
     fn apply(&self, delta: &Delta, interval: Interval) -> Option<Delta>;
 }

+ 87 - 42
rust-lib/flowy-ot/src/client/view/format_ext.rs

@@ -1,5 +1,5 @@
 use crate::{
-    client::view::{FormatExt, NEW_LINE},
+    client::view::{util::find_newline, FormatExt, NEW_LINE},
     core::{
         Attribute,
         AttributeKey,
@@ -17,6 +17,8 @@ use crate::{
 pub struct FormatLinkAtCaretPositionExt {}
 
 impl FormatExt for FormatLinkAtCaretPositionExt {
+    fn ext_name(&self) -> &str { "FormatLinkAtCaretPositionExt" }
+
     fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option<Delta> {
         if attribute.key != AttributeKey::Link || interval.size() != 0 {
             return None;
@@ -57,26 +59,56 @@ impl FormatExt for FormatLinkAtCaretPositionExt {
     }
 }
 
-pub struct ResolveLineFormatExt {}
-impl FormatExt for ResolveLineFormatExt {
+pub struct ResolveBlockFormatExt {}
+impl FormatExt for ResolveBlockFormatExt {
+    fn ext_name(&self) -> &str { "ResolveBlockFormatExt" }
+
     fn apply(&self, delta: &Delta, interval: Interval, attribute: &Attribute) -> Option<Delta> {
         if attribute.scope != AttributeScope::Block {
             return None;
         }
 
-        let mut new_delta = Delta::new();
-        new_delta.retain(interval.start, Attributes::default());
-
+        let mut new_delta = DeltaBuilder::new().retain(interval.start).build();
         let mut iter = DeltaIter::new(delta);
         iter.seek::<CharMetric>(interval.start);
+        let mut start = 0;
+        let end = interval.size();
+        while start < end && iter.has_next() {
+            let next_op = iter.next_op_with_len(end - start).unwrap();
+            match find_newline(next_op.get_data()) {
+                None => new_delta.retain(next_op.length(), Attributes::empty()),
+                Some(_) => {
+                    let tmp_delta = line_break(&next_op, attribute, AttributeScope::Block);
+                    new_delta.extend(tmp_delta);
+                },
+            }
+
+            start += next_op.length();
+        }
+
+        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.length(), Attributes::empty()),
+                Some(line_break) => {
+                    debug_assert_eq!(line_break, 0);
+                    new_delta.retain(1, attribute.clone().into());
+                    break;
+                },
+            }
+        }
 
-        None
+        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<Delta> {
         if attribute.scope != AttributeScope::Inline {
             return None;
@@ -85,44 +117,57 @@ impl FormatExt for ResolveInlineFormatExt {
         let mut iter = DeltaIter::new(delta);
         iter.seek::<CharMetric>(interval.start);
 
-        let mut cur = 0;
-        let len = interval.size();
-
-        while cur < len && iter.has_next() {
-            let some_op = iter.next_op_with_len(len - cur);
-            if some_op.is_none() {
-                return Some(new_delta);
-            }
-            let op = some_op.unwrap();
-            if let Operation::Insert(insert) = &op {
-                let mut s = insert.s.as_str();
-                match s.find(NEW_LINE) {
-                    None => {
-                        new_delta.retain(op.length(), attribute.clone().into());
-                    },
-                    Some(line_break) => {
-                        let mut pos = 0;
-                        let mut some_line_break = Some(line_break);
-                        while some_line_break.is_some() {
-                            let line_break = some_line_break.unwrap();
-                            new_delta.retain(line_break - pos, attribute.clone().into());
-                            new_delta.retain(1, Attributes::default());
-                            pos = line_break + 1;
-
-                            s = &s[pos..s.len()];
-                            some_line_break = s.find(NEW_LINE);
-                        }
-
-                        if pos < op.length() {
-                            new_delta.retain(op.length() - pos, attribute.clone().into());
-                        }
-                    },
-                }
+        let mut start = 0;
+        let end = interval.size();
+
+        while start < end && iter.has_next() {
+            let next_op = iter.next_op_with_len(end - start).unwrap();
+            match find_newline(next_op.get_data()) {
+                None => new_delta.retain(next_op.length(), attribute.clone().into()),
+                Some(_) => {
+                    let tmp_delta = line_break(&next_op, attribute, AttributeScope::Inline);
+                    new_delta.extend(tmp_delta);
+                },
             }
 
-            cur += op.length();
+            start += next_op.length();
         }
 
         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.length();
+    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
+}

+ 37 - 5
rust-lib/flowy-ot/src/client/view/insert_ext.rs

@@ -1,12 +1,14 @@
 use crate::{
     client::view::InsertExt,
-    core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter},
+    core::{AttributeKey, Attributes, CharMetric, Delta, DeltaBuilder, DeltaIter, Operation},
 };
 
 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,
@@ -20,6 +22,8 @@ impl InsertExt for PreserveBlockStyleOnInsertExt {
 
 pub struct PreserveLineStyleOnSplitExt {}
 impl InsertExt for PreserveLineStyleOnSplitExt {
+    fn ext_name(&self) -> &str { "PreserveLineStyleOnSplitExt" }
+
     fn apply(
         &self,
         _delta: &Delta,
@@ -34,6 +38,8 @@ impl InsertExt for PreserveLineStyleOnSplitExt {
 pub struct AutoExitBlockExt {}
 
 impl InsertExt for AutoExitBlockExt {
+    fn ext_name(&self) -> &str { "AutoExitBlockExt" }
+
     fn apply(
         &self,
         _delta: &Delta,
@@ -47,6 +53,8 @@ impl InsertExt for AutoExitBlockExt {
 
 pub struct InsertEmbedsExt {}
 impl InsertExt for InsertEmbedsExt {
+    fn ext_name(&self) -> &str { "InsertEmbedsExt" }
+
     fn apply(
         &self,
         _delta: &Delta,
@@ -60,6 +68,8 @@ impl InsertExt for InsertEmbedsExt {
 
 pub struct ForceNewlineForInsertsAroundEmbedExt {}
 impl InsertExt for ForceNewlineForInsertsAroundEmbedExt {
+    fn ext_name(&self) -> &str { "ForceNewlineForInsertsAroundEmbedExt" }
+
     fn apply(
         &self,
         _delta: &Delta,
@@ -73,6 +83,8 @@ impl InsertExt for ForceNewlineForInsertsAroundEmbedExt {
 
 pub struct AutoFormatLinksExt {}
 impl InsertExt for AutoFormatLinksExt {
+    fn ext_name(&self) -> &str { "AutoFormatLinksExt" }
+
     fn apply(
         &self,
         _delta: &Delta,
@@ -86,8 +98,10 @@ impl InsertExt for AutoFormatLinksExt {
 
 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<Delta> {
-        if text.ends_with(NEW_LINE) {
+        if text.contains(NEW_LINE) {
             return None;
         }
 
@@ -119,6 +133,8 @@ impl InsertExt for PreserveInlineStylesExt {
 
 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<Delta> {
         if text != NEW_LINE {
             return None;
@@ -133,7 +149,7 @@ impl InsertExt for ResetLineFormatOnNewLineExt {
 
         let mut reset_attribute = Attributes::new();
         if next_op.get_attributes().contains_key(&AttributeKey::Header) {
-            reset_attribute.add(AttributeKey::Header.with_value(""));
+            reset_attribute.add(AttributeKey::Header.value(""));
         }
 
         let len = index + replace_len;
@@ -150,11 +166,27 @@ impl InsertExt for ResetLineFormatOnNewLineExt {
 
 pub struct DefaultInsertExt {}
 impl InsertExt for DefaultInsertExt {
-    fn apply(&self, _delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option<Delta> {
+    fn ext_name(&self) -> &str { "DefaultInsertExt" }
+
+    fn apply(&self, delta: &Delta, replace_len: usize, text: &str, index: usize) -> Option<Delta> {
+        let mut iter = DeltaIter::new(delta);
+        let mut attributes = Attributes::new();
+
+        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(text)
+                .insert_with_attributes(text, attributes)
                 .build(),
         )
     }

+ 1 - 0
rust-lib/flowy-ot/src/client/view/mod.rs

@@ -2,6 +2,7 @@ mod delete_ext;
 mod extension;
 mod format_ext;
 mod insert_ext;
+mod util;
 mod view;
 
 pub use delete_ext::*;

+ 8 - 0
rust-lib/flowy-ot/src/client/view/util.rs

@@ -0,0 +1,8 @@
+use crate::client::view::NEW_LINE;
+
+pub fn find_newline(s: &str) -> Option<usize> {
+    match s.find(NEW_LINE) {
+        None => None,
+        Some(line_break) => Some(line_break),
+    }
+}

+ 5 - 2
rust-lib/flowy-ot/src/client/view/view.rs

@@ -1,6 +1,6 @@
 use crate::{
     client::view::*,
-    core::{Attribute, Delta, Interval},
+    core::{Attribute, Delta, Interval, Operation},
     errors::{ErrorBuilder, OTError, OTErrorCode},
 };
 
@@ -28,6 +28,7 @@ impl View {
         let mut new_delta = None;
         for ext in &self.insert_exts {
             if let Some(delta) = ext.apply(delta, interval.size(), text, interval.start) {
+                log::debug!("[{}]: applied, delta: {}", ext.ext_name(), delta);
                 new_delta = Some(delta);
                 break;
             }
@@ -43,6 +44,7 @@ impl View {
         let mut new_delta = None;
         for ext in &self.delete_exts {
             if let Some(delta) = ext.apply(delta, interval) {
+                log::debug!("[{}]: applied, delta: {}", ext.ext_name(), delta);
                 new_delta = Some(delta);
                 break;
             }
@@ -63,6 +65,7 @@ impl View {
         let mut new_delta = None;
         for ext in &self.format_exts {
             if let Some(delta) = ext.apply(delta, interval, &attribute) {
+                log::debug!("[{}]: applied, delta: {}", ext.ext_name(), delta);
                 new_delta = Some(delta);
                 break;
             }
@@ -92,7 +95,7 @@ fn construct_insert_exts() -> Vec<InsertExtension> {
 fn construct_format_exts() -> Vec<FormatExtension> {
     vec![
         Box::new(FormatLinkAtCaretPositionExt {}),
-        Box::new(ResolveLineFormatExt {}),
+        Box::new(ResolveBlockFormatExt {}),
         Box::new(ResolveInlineFormatExt {}),
     ]
 }

+ 6 - 5
rust-lib/flowy-ot/src/core/attributes/attributes.rs

@@ -1,14 +1,14 @@
-use crate::core::{Attribute, AttributeKey, Operation};
+use crate::core::{Attribute, AttributeKey, AttributeValue, Operation};
 use std::{collections::HashMap, fmt};
 
 pub const REMOVE_FLAG: &'static str = "";
-pub(crate) fn should_remove(s: &str) -> bool { s == REMOVE_FLAG }
+pub(crate) fn should_remove(val: &AttributeValue) -> bool { val.0 == REMOVE_FLAG }
 
 #[derive(Debug, Clone, Default, PartialEq, serde::Serialize, serde::Deserialize)]
 pub struct Attributes {
     #[serde(skip_serializing_if = "HashMap::is_empty")]
     #[serde(flatten)]
-    pub(crate) inner: HashMap<AttributeKey, String>,
+    pub(crate) inner: HashMap<AttributeKey, AttributeValue>,
 }
 
 impl fmt::Display for Attributes {
@@ -38,7 +38,8 @@ impl Attributes {
     }
 
     pub fn remove(&mut self, key: &AttributeKey) {
-        self.inner.insert(key.clone(), REMOVE_FLAG.to_owned());
+        let value: AttributeValue = REMOVE_FLAG.into();
+        self.inner.insert(key.clone(), value);
     }
 
     // Remove the key if its value is empty. e.g. { bold: "" }
@@ -62,7 +63,7 @@ impl Attributes {
 }
 
 impl std::ops::Deref for Attributes {
-    type Target = HashMap<AttributeKey, String>;
+    type Target = HashMap<AttributeKey, AttributeValue>;
 
     fn deref(&self) -> &Self::Target { &self.inner }
 }

+ 39 - 0
rust-lib/flowy-ot/src/core/attributes/attributes_serde.rs

@@ -0,0 +1,39 @@
+use crate::core::AttributeValue;
+use serde::{de, de::Visitor, Deserialize, Deserializer, Serialize, Serializer};
+use std::fmt;
+
+impl Serialize for AttributeValue {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        serializer.serialize_str(&self.0)
+    }
+}
+
+impl<'de> Deserialize<'de> for AttributeValue {
+    fn deserialize<D>(deserializer: D) -> Result<AttributeValue, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct OperationSeqVisitor;
+
+        impl<'de> Visitor<'de> for OperationSeqVisitor {
+            type Value = AttributeValue;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("a string")
+            }
+
+            fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
+            where
+                E: de::Error,
+            {
+                let attribute_value = AttributeValue(s.to_owned());
+                Ok(attribute_value)
+            }
+        }
+
+        deserializer.deserialize_str(OperationSeqVisitor)
+    }
+}

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

@@ -2,6 +2,13 @@ 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 {
@@ -23,8 +30,6 @@ pub enum AttributeKey {
     Color,
     #[display(fmt = "background")]
     Background,
-    #[display(fmt = "header")]
-    Header,
     #[display(fmt = "ident")]
     Ident,
     #[display(fmt = "align")]
@@ -41,18 +46,8 @@ pub enum AttributeKey {
     Height,
     #[display(fmt = "style")]
     Style,
-    #[display(fmt = "h1")]
-    H1,
-    #[display(fmt = "h2")]
-    H2,
-    #[display(fmt = "h3")]
-    H3,
-    #[display(fmt = "h4")]
-    H4,
-    #[display(fmt = "h5")]
-    H5,
-    #[display(fmt = "h6")]
-    H6,
+    #[display(fmt = "header")]
+    Header,
     #[display(fmt = "left")]
     LeftAlignment,
     #[display(fmt = "center")]
@@ -82,13 +77,13 @@ pub enum AttributeScope {
 #[derive(Debug, Clone)]
 pub struct Attribute {
     pub key: AttributeKey,
-    pub value: String,
+    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, self.scope);
+        let s = format!("{:?}:{} {:?}", self.key, self.value.as_ref(), self.scope);
         f.write_str(&s)
     }
 }
@@ -129,13 +124,13 @@ impl AttrsBuilder {
         self
     }
 
-    pub fn insert<T: Into<String>>(mut self, key: AttributeKey, value: T) -> Self {
-        self.inner.add(key.with_value(value));
+    pub fn insert<T: Into<AttributeValue>>(mut self, key: AttributeKey, value: T) -> Self {
+        self.inner.add(key.value(value));
         self
     }
 
     pub fn remove<T: Into<String>>(mut self, key: AttributeKey) -> Self {
-        self.inner.add(key.with_value(REMOVE_FLAG));
+        self.inner.add(key.value(REMOVE_FLAG));
         self
     }
 
@@ -148,9 +143,11 @@ impl AttrsBuilder {
 }
 
 impl AttributeKey {
-    pub fn with_value<T: Into<String>>(&self, value: T) -> Attribute {
+    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: String = value.into();
+        let value: AttributeValue = value.into();
         match self {
             AttributeKey::Bold
             | AttributeKey::Italic
@@ -167,12 +164,6 @@ impl AttributeKey {
             },
 
             AttributeKey::Header
-            | AttributeKey::H1
-            | AttributeKey::H2
-            | AttributeKey::H3
-            | AttributeKey::H4
-            | AttributeKey::H5
-            | AttributeKey::H6
             | AttributeKey::LeftAlignment
             | AttributeKey::CenterAlignment
             | AttributeKey::RightAlignment
@@ -199,3 +190,21 @@ impl AttributeKey {
         }
     }
 }
+
+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 - 0
rust-lib/flowy-ot/src/core/attributes/mod.rs

@@ -1,4 +1,5 @@
 mod attributes;
+mod attributes_serde;
 mod builder;
 
 pub use attributes::*;

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

@@ -42,11 +42,16 @@ impl<'a> Cursor<'a> {
             next_op = find_next_op(self);
         }
 
+        let old_c_index = self.c_index;
         while find_op.is_none() && next_op.is_some() {
             let op = next_op.take().unwrap();
             let interval = self.next_op_interval_with_constraint(force_len);
-            find_op = op.shrink(interval);
+            if interval.is_empty() {
+                self.next_op = Some(op.clone());
+                break;
+            }
 
+            find_op = op.shrink(interval);
             let suffix = Interval::new(0, op.length()).suffix(interval);
             if !suffix.is_empty() {
                 self.next_op = op.shrink(suffix);
@@ -60,7 +65,15 @@ impl<'a> Cursor<'a> {
             }
         }
 
-        find_op
+        if find_op.is_some() {
+            let last = self.c_index - old_c_index;
+            let force_len = force_len.unwrap_or(0);
+            if force_len > last {
+                let len = force_len - last;
+                return self.next_op_with_len(Some(len));
+            }
+        }
+        return find_op;
     }
 
     pub fn next_op(&mut self) -> Option<Operation> { self.next_op_with_len(None) }
@@ -159,6 +172,7 @@ impl Metric for CharMetric {
     fn seek(cursor: &mut Cursor, index: usize) -> SeekResult {
         let _ = check_bound(cursor.c_index, index)?;
         let _ = cursor.next_op_with_len(Some(index));
+
         Ok(())
     }
 }

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

@@ -599,6 +599,8 @@ impl Delta {
     pub fn is_empty(&self) -> bool { self.ops.is_empty() }
 
     pub fn to_json(&self) -> String { serde_json::to_string(self).unwrap_or("".to_owned()) }
+
+    pub fn extend(&mut self, other: Self) { other.ops.into_iter().for_each(|op| self.add(op)); }
 }
 
 fn invert_from_other(

+ 53 - 0
rust-lib/flowy-ot/src/core/delta/delta_serde.rs

@@ -0,0 +1,53 @@
+use crate::core::Delta;
+use serde::{
+    de::{SeqAccess, Visitor},
+    ser::SerializeSeq,
+    Deserialize,
+    Deserializer,
+    Serialize,
+    Serializer,
+};
+use std::fmt;
+
+impl Serialize for Delta {
+    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+    where
+        S: Serializer,
+    {
+        let mut seq = serializer.serialize_seq(Some(self.ops.len()))?;
+        for op in self.ops.iter() {
+            seq.serialize_element(op)?;
+        }
+        seq.end()
+    }
+}
+
+impl<'de> Deserialize<'de> for Delta {
+    fn deserialize<D>(deserializer: D) -> Result<Delta, D::Error>
+    where
+        D: Deserializer<'de>,
+    {
+        struct OperationSeqVisitor;
+
+        impl<'de> Visitor<'de> for OperationSeqVisitor {
+            type Value = Delta;
+
+            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
+                formatter.write_str("a sequence")
+            }
+
+            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
+            where
+                A: SeqAccess<'de>,
+            {
+                let mut o = Delta::default();
+                while let Some(op) = seq.next_element()? {
+                    o.add(op);
+                }
+                Ok(o)
+            }
+        }
+
+        deserializer.deserialize_seq(OperationSeqVisitor)
+    }
+}

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

@@ -1,6 +1,7 @@
 mod builder;
 mod cursor;
 mod delta;
+mod delta_serde;
 mod iterator;
 
 pub use builder::*;

+ 0 - 43
rust-lib/flowy-ot/src/core/operation/operation_serde.rs

@@ -92,46 +92,3 @@ impl<'de> Deserialize<'de> for Operation {
         deserializer.deserialize_any(OperationVisitor)
     }
 }
-
-impl Serialize for Delta {
-    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
-    where
-        S: Serializer,
-    {
-        let mut seq = serializer.serialize_seq(Some(self.ops.len()))?;
-        for op in self.ops.iter() {
-            seq.serialize_element(op)?;
-        }
-        seq.end()
-    }
-}
-
-impl<'de> Deserialize<'de> for Delta {
-    fn deserialize<D>(deserializer: D) -> Result<Delta, D::Error>
-    where
-        D: Deserializer<'de>,
-    {
-        struct OperationSeqVisitor;
-
-        impl<'de> Visitor<'de> for OperationSeqVisitor {
-            type Value = Delta;
-
-            fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
-                formatter.write_str("a sequence")
-            }
-
-            fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
-            where
-                A: SeqAccess<'de>,
-            {
-                let mut o = Delta::default();
-                while let Some(op) = seq.next_element()? {
-                    o.add(op);
-                }
-                Ok(o)
-            }
-        }
-
-        deserializer.deserialize_seq(OperationSeqVisitor)
-    }
-}

+ 113 - 31
rust-lib/flowy-ot/tests/attribute_test.rs

@@ -4,7 +4,7 @@ use crate::helper::{TestOp::*, *};
 use flowy_ot::core::Interval;
 
 #[test]
-fn delta_insert_text() {
+fn attributes_insert_text() {
     let ops = vec![
         Insert(0, "123", 0),
         Insert(0, "456", 3),
@@ -14,7 +14,7 @@ fn delta_insert_text() {
 }
 
 #[test]
-fn delta_insert_text_at_head() {
+fn attributes_insert_text_at_head() {
     let ops = vec![
         Insert(0, "123", 0),
         Insert(0, "456", 0),
@@ -24,7 +24,7 @@ fn delta_insert_text_at_head() {
 }
 
 #[test]
-fn delta_insert_text_at_middle() {
+fn attributes_insert_text_at_middle() {
     let ops = vec![
         Insert(0, "123", 0),
         Insert(0, "456", 1),
@@ -34,7 +34,7 @@ fn delta_insert_text_at_middle() {
 }
 
 #[test]
-fn delta_insert_text_with_attr() {
+fn attributes_insert_text_with_attr() {
     let ops = vec![
         Insert(0, "145", 0),
         Insert(0, "23", 1),
@@ -53,7 +53,7 @@ fn delta_insert_text_with_attr() {
 }
 
 #[test]
-fn delta_add_bold() {
+fn attributes_add_bold() {
     let ops = vec![
         Insert(0, "123456", 0),
         Bold(0, Interval::new(3, 5), true),
@@ -70,7 +70,7 @@ fn delta_add_bold() {
 }
 
 #[test]
-fn delta_add_bold_and_invert_all() {
+fn attributes_add_bold_and_invert_all() {
     let ops = vec![
         Insert(0, "123", 0),
         Bold(0, Interval::new(0, 3), true),
@@ -82,7 +82,7 @@ fn delta_add_bold_and_invert_all() {
 }
 
 #[test]
-fn delta_add_bold_and_invert_partial_suffix() {
+fn attributes_add_bold_and_invert_partial_suffix() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
@@ -97,7 +97,7 @@ fn delta_add_bold_and_invert_partial_suffix() {
 }
 
 #[test]
-fn delta_add_bold_and_invert_partial_suffix2() {
+fn attributes_add_bold_and_invert_partial_suffix2() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
@@ -114,7 +114,35 @@ fn delta_add_bold_and_invert_partial_suffix2() {
 }
 
 #[test]
-fn delta_add_bold_and_invert_partial_prefix() {
+fn attributes_add_bold_with_new_line() {
+    let ops = vec![
+        Insert(0, "123456", 0),
+        Bold(0, Interval::new(0, 6), true),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123456","attributes":{"bold":"true"}},{"insert":"\n"}]"#,
+        ),
+        Insert(0, "\n", 3),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123","attributes":{"bold":"true"}},{"insert":"\n"},{"insert":"456","attributes":{"bold":"true"}},{"insert":"\n"}]"#,
+        ),
+        Insert(0, "\n", 4),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123","attributes":{"bold":"true"}},{"insert":"\n\n"},{"insert":"456","attributes":{"bold":"true"}},{"insert":"\n"}]"#,
+        ),
+        Insert(0, "a", 4),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123","attributes":{"bold":"true"}},{"insert":"\na\n"},{"insert":"456","attributes":{"bold":"true"}},{"insert":"\n"}]"#,
+        ),
+    ];
+    OpTester::new().run_script_with_newline(ops);
+}
+
+#[test]
+fn attributes_add_bold_and_invert_partial_prefix() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
@@ -129,7 +157,7 @@ fn delta_add_bold_and_invert_partial_prefix() {
 }
 
 #[test]
-fn delta_add_bold_consecutive() {
+fn attributes_add_bold_consecutive() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 1), true),
@@ -147,31 +175,26 @@ fn delta_add_bold_consecutive() {
 }
 
 #[test]
-fn delta_add_bold_italic() {
+fn attributes_add_bold_italic() {
     let ops = vec![
         Insert(0, "1234", 0),
         Bold(0, Interval::new(0, 4), true),
         Italic(0, Interval::new(0, 4), true),
         AssertOpsJson(
             0,
-            r#"[{"insert":"1234","attributes":{"italic":"true","bold":"true"}}]"#,
+            r#"[{"insert":"1234","attributes":{"italic":"true","bold":"true"}},{"insert":"\n"}]"#,
         ),
         Insert(0, "5678", 4),
         AssertOpsJson(
             0,
-            r#"[{"insert":"12345678","attributes":{"italic":"true","bold":"true"}}]"#,
-        ),
-        Italic(0, Interval::new(4, 6), false),
-        AssertOpsJson(
-            0,
-            r#"[{"insert":"1234","attributes":{"bold":"true","italic":"true"}},{"insert":"56","attributes":{"bold":"true"}},{"insert":"78","attributes":{"bold":"true","italic":"true"}}]"#,
+            r#"[{"insert":"12345678","attributes":{"bold":"true","italic":"true"}},{"insert":"\n"}]"#,
         ),
     ];
-    OpTester::new().run_script(ops);
+    OpTester::new().run_script_with_newline(ops);
 }
 
 #[test]
-fn delta_add_bold_italic2() {
+fn attributes_add_bold_italic2() {
     let ops = vec![
         Insert(0, "123456", 0),
         Bold(0, Interval::new(0, 6), true),
@@ -199,7 +222,7 @@ fn delta_add_bold_italic2() {
 }
 
 #[test]
-fn delta_add_bold_italic3() {
+fn attributes_add_bold_italic3() {
     let ops = vec![
         Insert(0, "123456789", 0),
         Bold(0, Interval::new(0, 5), true),
@@ -236,7 +259,7 @@ fn delta_add_bold_italic3() {
 }
 
 #[test]
-fn delta_add_bold_italic_delete() {
+fn attributes_add_bold_italic_delete() {
     let ops = vec![
         Insert(0, "123456789", 0),
         Bold(0, Interval::new(0, 5), true),
@@ -275,7 +298,7 @@ fn delta_add_bold_italic_delete() {
 }
 
 #[test]
-fn delta_merge_inserted_text_with_same_attribute() {
+fn attributes_merge_inserted_text_with_same_attribute() {
     let ops = vec![
         InsertBold(0, "123", Interval::new(0, 3)),
         AssertOpsJson(0, r#"[{"insert":"123","attributes":{"bold":"true"}}]"#),
@@ -286,7 +309,7 @@ fn delta_merge_inserted_text_with_same_attribute() {
 }
 
 #[test]
-fn delta_compose_attr_delta_with_attr_delta_test() {
+fn attributes_compose_attr_attributes_with_attr_attributes_test() {
     let ops = vec![
         InsertBold(0, "123456", Interval::new(0, 6)),
         AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
@@ -301,7 +324,7 @@ fn delta_compose_attr_delta_with_attr_delta_test() {
 }
 
 #[test]
-fn delta_compose_attr_delta_with_attr_delta_test2() {
+fn attributes_compose_attr_attributes_with_attr_attributes_test2() {
     let ops = vec![
         Insert(0, "123456", 0),
         Bold(0, Interval::new(0, 6), true),
@@ -342,7 +365,7 @@ fn delta_compose_attr_delta_with_attr_delta_test2() {
 }
 
 #[test]
-fn delta_compose_attr_delta_with_no_attr_delta_test() {
+fn attributes_compose_attr_attributes_with_no_attr_attributes_test() {
     let expected = r#"[{"insert":"123456","attributes":{"bold":"true"}},{"insert":"7"}]"#;
 
     let ops = vec![
@@ -358,7 +381,7 @@ fn delta_compose_attr_delta_with_no_attr_delta_test() {
 }
 
 #[test]
-fn delta_replace_heading() {
+fn attributes_replace_heading() {
     let ops = vec![
         InsertBold(0, "123456", Interval::new(0, 6)),
         AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
@@ -370,7 +393,7 @@ fn delta_replace_heading() {
 }
 
 #[test]
-fn delta_replace_trailing() {
+fn attributes_replace_trailing() {
     let ops = vec![
         InsertBold(0, "123456", Interval::new(0, 6)),
         AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
@@ -382,7 +405,7 @@ fn delta_replace_trailing() {
 }
 
 #[test]
-fn delta_replace_middle() {
+fn attributes_replace_middle() {
     let ops = vec![
         InsertBold(0, "123456", Interval::new(0, 6)),
         AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
@@ -396,7 +419,7 @@ fn delta_replace_middle() {
 }
 
 #[test]
-fn delta_replace_all() {
+fn attributes_replace_all() {
     let ops = vec![
         InsertBold(0, "123456", Interval::new(0, 6)),
         AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
@@ -408,7 +431,7 @@ fn delta_replace_all() {
 }
 
 #[test]
-fn delta_replace_with_text() {
+fn attributes_replace_with_text() {
     let ops = vec![
         InsertBold(0, "123456", Interval::new(0, 6)),
         AssertOpsJson(0, r#"[{"insert":"123456","attributes":{"bold":"true"}}]"#),
@@ -421,3 +444,62 @@ fn delta_replace_with_text() {
 
     OpTester::new().run_script(ops);
 }
+
+#[test]
+fn attributes_add_header() {
+    let ops = vec![
+        Insert(0, "123456", 0),
+        Header(0, Interval::new(0, 6), 1, true),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123456"},{"insert":"\n","attributes":{"header":"1"}}]"#,
+        ),
+        Insert(0, "\n", 3),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123"},{"insert":"\n","attributes":{"header":"1"}},{"insert":"456"},{"insert":"\n","attributes":{"header":"1"}}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}
+
+#[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() {
+    let ops = vec![
+        Insert(0, "123456", 0),
+        Header(0, Interval::new(0, 6), 1, true),
+        Insert(0, "\n", 3),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123"},{"insert":"\n","attributes":{"header":"1"}},{"insert":"456"},{"insert":"\n","attributes":{"header":"1"}}]"#,
+        ),
+        Insert(0, "\n", 4),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123"},{"insert":"\n\n","attributes":{"header":"1"}},{"insert":"456"},{"insert":"\n","attributes":{"header":"1"}}]"#,
+        ),
+        Insert(0, "\n", 4),
+        AssertOpsJson(
+            0,
+            r#"[{"insert":"123"},{"insert":"\n\n","attributes":{"header":"1"}},{"insert":"\n456"},{"insert":"\n","attributes":{"header":"1"}}]"#,
+        ),
+    ];
+
+    OpTester::new().run_script_with_newline(ops);
+}

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

@@ -28,6 +28,9 @@ pub enum TestOp {
     #[display(fmt = "Italic")]
     Italic(usize, Interval, bool),
 
+    #[display(fmt = "Header")]
+    Header(usize, Interval, usize, bool),
+
     #[display(fmt = "Transform")]
     Transform(usize, usize),
 
@@ -60,7 +63,7 @@ impl OpTester {
         static INIT: Once = Once::new();
         INIT.call_once(|| {
             color_eyre::install().unwrap();
-            std::env::set_var("RUST_LOG", "info");
+            std::env::set_var("RUST_LOG", "debug");
             env_logger::init();
         });
 
@@ -86,22 +89,30 @@ impl OpTester {
                 let document = &mut self.documents[*delta_i];
                 document.insert(interval.start, s).unwrap();
                 document
-                    .format(*interval, AttributeKey::Bold.with_value("true".to_owned()))
+                    .format(*interval, AttributeKey::Bold.value(true))
                     .unwrap();
             },
             TestOp::Bold(delta_i, interval, enable) => {
                 let document = &mut self.documents[*delta_i];
                 let attribute = match *enable {
-                    true => AttributeKey::Bold.with_value("true".to_owned()),
-                    false => AttributeKey::Bold.with_value("".to_owned()),
+                    true => AttributeKey::Bold.value(true),
+                    false => AttributeKey::Bold.remove(),
                 };
                 document.format(*interval, attribute).unwrap();
             },
             TestOp::Italic(delta_i, interval, enable) => {
                 let document = &mut self.documents[*delta_i];
                 let attribute = match *enable {
-                    true => AttributeKey::Italic.with_value("true"),
-                    false => AttributeKey::Italic.with_value(REMOVE_FLAG),
+                    true => AttributeKey::Italic.value("true"),
+                    false => AttributeKey::Italic.remove(),
+                };
+                document.format(*interval, attribute).unwrap();
+            },
+            TestOp::Header(delta_i, interval, 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();
             },

+ 20 - 0
rust-lib/flowy-ot/tests/op_test.rs

@@ -223,6 +223,26 @@ fn delta_seek_4() {
     );
 }
 
+#[test]
+fn delta_seek_5() {
+    let mut delta = Delta::default();
+    let attributes = AttrsBuilder::new().bold(true).italic(true).build();
+    delta.add(
+        OpBuilder::insert("1234")
+            .attributes(attributes.clone())
+            .build(),
+    );
+    delta.add(OpBuilder::insert("\n").build());
+
+    let mut iter = DeltaIter::new(&delta);
+    iter.seek::<CharMetric>(0);
+
+    assert_eq!(
+        iter.next_op_with_len(4).unwrap(),
+        OpBuilder::insert("1234").attributes(attributes).build(),
+    );
+}
+
 #[test]
 fn delta_next_op_len_test() {
     let mut delta = Delta::default();