refactor: move block delete to history-safe commands

This commit is contained in:
lix-2026
2026-05-24 23:56:25 +08:00
parent 437c690a39
commit f2a7de9896
6 changed files with 343 additions and 76 deletions
@@ -1,4 +1,8 @@
use leptos_tiptap::TiptapEditorHandle;
use leptos_tiptap::{
TiptapContent, TiptapEditorHandle, TiptapInsertContentOptions, TiptapRange,
};
use serde_json::json;
use serde_json::Value;
use crate::editor_runtime::attachment_links;
@@ -18,3 +22,92 @@ pub(crate) fn delete_selected_attachment_link_with_history(
// 继续走 Tiptap 命令路径,保留撤销/重做历史。
editor.delete_selection().map_err(|error| error.to_string())
}
fn prosemirror_node_size(node: &Value) -> Option<u32> {
match node.get("type").and_then(Value::as_str) {
Some("text") => Some(
node.get("text")
.and_then(Value::as_str)
.map(|text| text.encode_utf16().count() as u32)
.unwrap_or(0),
),
Some("hardBreak") | Some("horizontalRule") => Some(1),
_ => {
let Some(children) = node.get("content").and_then(Value::as_array) else {
return Some(match node.get("type").and_then(Value::as_str) {
Some("paragraph") | Some("heading") | Some("blockquote")
| Some("codeBlock") | Some("bulletList") | Some("orderedList")
| Some("taskList") | Some("listItem") | Some("taskItem") | Some("table")
| Some("tableRow") | Some("tableCell") | Some("tableHeader") => 2,
_ => 1,
});
};
let content_size = children.iter().try_fold(0_u32, |acc, child| {
prosemirror_node_size(child).map(|size| acc + size)
})?;
Some(content_size + 2)
}
}
}
fn top_level_block_full_range(document: &Value, index: usize) -> Option<TiptapRange> {
let content = document.get("content").and_then(Value::as_array)?;
let mut position = 0_u32;
for (current_index, node) in content.iter().enumerate() {
let node_size = prosemirror_node_size(node)?;
if current_index == index {
return Some(TiptapRange {
from: position,
to: position + node_size,
});
}
position += node_size;
}
None
}
/// 通过 Tiptap `delete_range()` 删除顶层块,避免使用 `set_content()` 整文替换绕过 history。
pub(crate) fn delete_top_level_block_with_history(
editor: &TiptapEditorHandle,
index: usize,
) -> Result<(), String> {
let document = editor
.get_json()
.map_err(|err| format!("读取当前 JSON 失败:{err}"))?;
let content_len = document
.get("content")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
if index >= content_len {
return Err(format!("找不到第 {index} 个块"));
}
let range = top_level_block_full_range(&document, index)
.ok_or_else(|| format!("找不到第 {} 个块的删除范围", index + 1))?;
if content_len <= 1 {
editor
.insert_content_at(
range,
TiptapContent::json(json!({ "type": "paragraph" })),
Some(TiptapInsertContentOptions {
update_selection: Some(true),
..Default::default()
}),
)
.map_err(|err| format!("删除当前块失败:{err}"))?;
} else {
editor
.delete_range(range)
.map_err(|err| format!("删除当前块失败:{err}"))?;
}
editor
.focus()
.map_err(|err| format!("聚焦编辑器失败:{err}"))
}