Skip to content

Commit ca76d86

Browse files
authored
fix: LoroText attach error (#873)
* fix: LoroText attach error * chore: simplify the fix * chore: changeset * refactor: rm extra code
1 parent 3927c1a commit ca76d86

File tree

4 files changed

+191
-34
lines changed

4 files changed

+191
-34
lines changed

.changeset/slow-islands-fold.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"loro-crdt": patch
3+
"loro-crdt-map": patch
4+
---
5+
6+
fix: Empty LoroText attach error #873

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/loro-internal/src/handler.rs

Lines changed: 85 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1965,20 +1965,10 @@ impl TextHandler {
19651965
match &self.inner {
19661966
MaybeDetached::Detached(t) => {
19671967
let mut g = t.lock().unwrap();
1968-
self.mark_for_detached(
1969-
&mut g.value,
1970-
key,
1971-
&value,
1972-
start,
1973-
end,
1974-
pos_type,
1975-
false,
1976-
)
1968+
self.mark_for_detached(&mut g.value, key, &value, start, end, pos_type, false)
19771969
}
19781970
MaybeDetached::Attached(a) => {
1979-
a.with_txn(|txn| {
1980-
self.mark_with_txn(txn, start, end, key, value, pos_type, false)
1981-
})
1971+
a.with_txn(|txn| self.mark_with_txn(txn, start, end, key, value, pos_type, false))
19821972
}
19831973
}
19841974
}
@@ -2052,11 +2042,9 @@ impl TextHandler {
20522042
pos_type,
20532043
true,
20542044
),
2055-
MaybeDetached::Attached(a) => {
2056-
a.with_txn(|txn| {
2057-
self.mark_with_txn(txn, start, end, key, LoroValue::Null, pos_type, true)
2058-
})
2059-
}
2045+
MaybeDetached::Attached(a) => a.with_txn(|txn| {
2046+
self.mark_with_txn(txn, start, end, key, LoroValue::Null, pos_type, true)
2047+
}),
20602048
}
20612049
}
20622050

@@ -2082,10 +2070,7 @@ impl TextHandler {
20822070

20832071
let mut doc_state = inner.doc.state.lock().unwrap();
20842072
let len = doc_state.with_state_mut(inner.container_idx, |state| {
2085-
state
2086-
.as_richtext_state_mut()
2087-
.unwrap()
2088-
.len(pos_type)
2073+
state.as_richtext_state_mut().unwrap().len(pos_type)
20892074
});
20902075

20912076
if end > len {
@@ -2204,12 +2189,23 @@ impl TextHandler {
22042189
for d in delta {
22052190
match d {
22062191
TextDelta::Insert { insert, attributes } => {
2207-
let end = index + event_len(insert.as_str());
2192+
let insert_len = event_len(insert.as_str());
2193+
if insert_len == 0 {
2194+
continue;
2195+
}
2196+
2197+
let mut empty_attr = None;
2198+
let attr_ref = attributes.as_ref().unwrap_or_else(|| {
2199+
empty_attr = Some(FxHashMap::default());
2200+
empty_attr.as_ref().unwrap()
2201+
});
2202+
2203+
let end = index + insert_len;
22082204
let override_styles = self.insert_with_txn_and_attr(
22092205
txn,
22102206
index,
22112207
insert.as_str(),
2212-
Some(attributes.as_ref().unwrap_or(&Default::default())),
2208+
Some(attr_ref),
22132209
PosType::Event,
22142210
)?;
22152211

@@ -2397,6 +2393,10 @@ impl TextHandler {
23972393
MaybeDetached::Detached(s) => {
23982394
let mut delta = Vec::new();
23992395
for span in s.lock().unwrap().value.iter() {
2396+
if span.text.as_str().is_empty() {
2397+
continue;
2398+
}
2399+
24002400
let next_attr = span.attributes.to_option_map();
24012401
match delta.last_mut() {
24022402
Some(TextDelta::Insert { insert, attributes })
@@ -2407,6 +2407,7 @@ impl TextHandler {
24072407
}
24082408
_ => {}
24092409
}
2410+
24102411
delta.push(TextDelta::Insert {
24112412
insert: span.text.as_str().to_string(),
24122413
attributes: next_attr,
@@ -4218,7 +4219,7 @@ mod test {
42184219
use crate::version::Frontiers;
42194220
use crate::LoroDoc;
42204221
use crate::{fx_map, ToJson};
4221-
use loro_common::ID;
4222+
use loro_common::{LoroValue, ID};
42224223
use serde_json::json;
42234224

42244225
#[test]
@@ -4333,15 +4334,7 @@ mod test {
43334334
let handler = loro.get_text("richtext");
43344335
handler.insert_with_txn(&mut txn, 0, "hello world").unwrap();
43354336
handler
4336-
.mark_with_txn(
4337-
&mut txn,
4338-
0,
4339-
5,
4340-
"bold",
4341-
true.into(),
4342-
PosType::Event,
4343-
false,
4344-
)
4337+
.mark_with_txn(&mut txn, 0, 5, "bold", true.into(), PosType::Event, false)
43454338
.unwrap();
43464339
txn.commit().unwrap();
43474340

@@ -4436,4 +4429,63 @@ mod test {
44364429
])
44374430
)
44384431
}
4432+
4433+
#[test]
4434+
fn richtext_apply_delta_marks_without_growth() {
4435+
let loro = LoroDoc::new_auto_commit();
4436+
let text = loro.get_text("text");
4437+
text.insert(0, "abc").unwrap();
4438+
4439+
text.apply_delta(&[TextDelta::Retain {
4440+
retain: 3,
4441+
attributes: Some(fx_map!("bold".into() => LoroValue::Bool(true))),
4442+
}])
4443+
.unwrap();
4444+
loro.commit_then_renew();
4445+
4446+
assert_eq!(text.to_string(), "abc");
4447+
assert_eq!(
4448+
text.get_richtext_value().to_json_value(),
4449+
json!([{"insert": "abc", "attributes": {"bold": true}}])
4450+
);
4451+
}
4452+
4453+
#[test]
4454+
fn richtext_apply_delta_grows_for_mark_gap() {
4455+
let loro = LoroDoc::new_auto_commit();
4456+
let text = loro.get_text("text");
4457+
4458+
text.apply_delta(&[TextDelta::Retain {
4459+
retain: 1,
4460+
attributes: Some(fx_map!("bold".into() => LoroValue::Bool(true))),
4461+
}])
4462+
.unwrap();
4463+
loro.commit_then_renew();
4464+
4465+
assert_eq!(text.to_string(), "\n");
4466+
assert_eq!(
4467+
text.get_richtext_value().to_json_value(),
4468+
json!([{"insert": "\n", "attributes": {"bold": true}}])
4469+
);
4470+
}
4471+
4472+
#[test]
4473+
fn richtext_apply_delta_ignores_empty_inserts() {
4474+
let loro = LoroDoc::new_auto_commit();
4475+
let text = loro.get_text("text");
4476+
text.insert(0, "seed").unwrap();
4477+
4478+
text.apply_delta(&[TextDelta::Insert {
4479+
insert: "".into(),
4480+
attributes: Some(fx_map!("bold".into() => LoroValue::Bool(true))),
4481+
}])
4482+
.unwrap();
4483+
loro.commit_then_renew();
4484+
4485+
assert_eq!(text.to_string(), "seed");
4486+
assert_eq!(
4487+
text.get_richtext_value().to_json_value(),
4488+
json!([{"insert": "seed"}])
4489+
);
4490+
}
44394491
}

crates/loro-wasm/tests/basic.test.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,3 +1851,102 @@ it("returns undefined when getting a non-existent cursor", () => {
18511851
const newDoc = new LoroDoc();
18521852
expect(newDoc.getCursorPos(cursor)).toBeUndefined();
18531853
});
1854+
1855+
it("should match when inserting container", () => {
1856+
const doc = new LoroDoc();
1857+
const list = doc.getList("list");
1858+
const text = new LoroText();
1859+
text.insert(0, "");
1860+
list.insertContainer(0, text);
1861+
1862+
const retrievedText = list.get(0) as LoroText;
1863+
expect(retrievedText.toString()).toBe("");
1864+
});
1865+
1866+
it("keeps detached text content when inserted", () => {
1867+
const doc = new LoroDoc();
1868+
const list = doc.getList("list");
1869+
const text = new LoroText();
1870+
text.insert(0, "detached");
1871+
1872+
list.insertContainer(0, text);
1873+
1874+
const retrievedText = list.get(0) as LoroText;
1875+
expect(retrievedText.toString()).toBe("detached");
1876+
});
1877+
1878+
it("keeps detached text styles when inserted", () => {
1879+
const doc = new LoroDoc();
1880+
const list = doc.getList("list");
1881+
const text = new LoroText();
1882+
text.insert(0, "styled");
1883+
text.mark({ start: 0, end: 6 }, "bold", true);
1884+
1885+
list.insertContainer(0, text);
1886+
1887+
const retrievedText = list.get(0) as LoroText;
1888+
expect(retrievedText.toDelta()).toStrictEqual([
1889+
{ insert: "styled", attributes: { bold: true } },
1890+
]);
1891+
});
1892+
1893+
it("keeps detached unicode text when inserted", () => {
1894+
const doc = new LoroDoc();
1895+
const list = doc.getList("list");
1896+
const text = new LoroText();
1897+
const content = "👨‍👩‍👦 family";
1898+
text.insert(0, content);
1899+
1900+
list.insertContainer(0, text);
1901+
1902+
const retrievedText = list.get(0) as LoroText;
1903+
expect(retrievedText.toString()).toBe(content);
1904+
});
1905+
1906+
it("keeps detached partial styles when inserted", () => {
1907+
const doc = new LoroDoc();
1908+
const list = doc.getList("list");
1909+
const text = new LoroText();
1910+
text.insert(0, "abcDEF");
1911+
text.mark({ start: 0, end: 3 }, "bold", true);
1912+
1913+
list.insertContainer(0, text);
1914+
1915+
const retrievedText = list.get(0) as LoroText;
1916+
expect(retrievedText.toDelta()).toStrictEqual([
1917+
{ insert: "abc", attributes: { bold: true } },
1918+
{ insert: "DEF" },
1919+
]);
1920+
});
1921+
1922+
it("copies attached text without sharing future edits", () => {
1923+
const doc = new LoroDoc();
1924+
const source = doc.getText("source");
1925+
source.insert(0, "root");
1926+
const list = doc.getList("list");
1927+
1928+
list.insertContainer(0, source);
1929+
const copied = list.get(0) as LoroText;
1930+
1931+
source.insert(4, "-updated");
1932+
expect(source.toString()).toBe("root-updated");
1933+
expect(copied.toString()).toBe("root");
1934+
});
1935+
1936+
it("throws when inserting an attached text from another doc", () => {
1937+
const docA = new LoroDoc();
1938+
const textA = docA.getText("text");
1939+
textA.insert(0, "cross");
1940+
1941+
const docB = new LoroDoc();
1942+
const listB = docB.getList("list");
1943+
1944+
expect(() => listB.insertContainer(0, textA)).toThrow();
1945+
});
1946+
1947+
it("apply empty delta", () => {
1948+
const doc = new LoroDoc();
1949+
const text = doc.getText("text");
1950+
text.applyDelta([{ insert: "" }]);
1951+
expect(text.toString()).toBe("");
1952+
});

0 commit comments

Comments
 (0)