refactor: extract editor action modules

This commit is contained in:
lix-2026
2026-05-25 19:50:50 +08:00
parent 7d5319606c
commit 4537b1a3d6
9 changed files with 536 additions and 509 deletions
@@ -186,7 +186,7 @@ codegraph sync .
|----|------|
| SIDEBAR_TREE_JS 外置 | 布局文件从 11,741 → ~1,100 行 |
| browser/ 模块 | 11 个 JS 文件,总计 ~11,000 行 |
| editor_runtime/ 模块 | 16 个 Rust 模块(另有 mod.rs |
| editor_runtime/ 模块 | 19 个 Rust 模块(另有 mod.rs |
| 浏览器 smoke | task479/488/489/451 全部 PASS |
| 测试 | editor 13/13, web sidebar 10/10, web full 531/549 |
| CodeGraph | 350+ 文件索引完成 |
@@ -195,4 +195,4 @@ codegraph sync .
| 项 | 说明 |
|----|------|
| Batch C 全量验证 | 尚未运行 `cargo test -p mnote-web --lib` / `cargo test -p mnote-leptos-tiptap-spike` / task451 全套;本轮已完成 spike check/build、wasm-bindgen、task479/task488/task489 与直接编辑器交互回归 |
| Batch C 全量验证 | 尚未运行 `cargo test -p mnote-web --lib` / `cargo test -p mnote-leptos-tiptap-spike` / task451 全套;本轮已完成 spike check/build、wasm-bindgen、task479/task488/task489、临时 slash/table Playwright 回归与直接编辑器交互回归 |
@@ -406,11 +406,14 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
| 文件 | 原大小 | 现大小 | 变化 |
|------|--------|--------|------|
| `lib.rs` | 9,873 行 | 8,671 行 | **-12%** |
| `lib.rs` | 9,873 行 | 8,181 行 | **-17%** |
| `editor_runtime/mindmap_node_view.rs` | — | 909 行 | 新建 |
| `editor_runtime/bridge_events.rs` | — | 149 行 | 新建 |
| `editor_runtime/slash_actions.rs` | — | 237 行 | 新建 |
| `editor_runtime/table_commands.rs` | — | 180 行 | 新建 |
| `editor_runtime/bridge_dispatch.rs` | — | 86 行 | 新建 |
### editor_runtime/ 最终模块清单(16 模块,另有 `mod.rs`
### editor_runtime/ 最终模块清单(19 模块,另有 `mod.rs`
| 模块 | 说明 | 来源 |
|------|------|------|
@@ -420,6 +423,7 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
| `block_hover_state.rs` | HoveredBlockState 共享类型 | 5-29 |
| `block_menu_document.rs` | legacy JSON helper | 5-29 |
| `block_menu_overlay.rs` | block menu 几何 | 5-29 |
| `bridge_dispatch.rs` | host command kind 解析 | **本批新建** |
| `bridge_events.rs` | 事件常量 + payload 类型 | **本批新建** |
| `command_sync.rs` | 命令同步 | 5-28 P2 |
| `content_layout.rs` | 内容布局 | 5-29 |
@@ -430,6 +434,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
| `mindmap_node_view.rs` | mindmap shell 完整实现 | **本批新建**5-28 P5 |
| `overlays.rs` | overlay 管理 | 5-29 |
| `persistence.rs` | 持久化 | 5-28 P2 |
| `slash_actions.rs` | slash 菜单 action 数据 | **本批新建** |
| `table_commands.rs` | table toolbar / option 命令 | **本批新建** |
### 未完成(后续独立任务)
@@ -448,3 +454,8 @@ UI / 浏览器可见项必须有真实浏览器截图或结构化 smoke 证据
- 拖拽重排两条路径与块菜单“拷贝副本”不再读取/写回整文 HTML,改为 `move_top_level_block_with_history` / `duplicate_top_level_block_with_history` + `sync_persisted_editor_command`
- 删除本轮变成无调用方的 `block_menu_legacy_html.rs``apply_html_update`generated island JS/WASM 已重新生成。
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check``cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml``cargo build --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml --target wasm32-unknown-unknown --release``wasm-bindgen --target web --out-dir rust/spikes/leptos-tiptap-spike/generated/island --out-name mnote-leptos-tiptap-spike-island rust/spikes/leptos-tiptap-spike/target/wasm32-unknown-unknown/release/mnote_leptos_tiptap_spike.wasm``task489``task488``task479`、临时 Playwright 拷贝副本 undo/redo + 块拖拽重排回归。
- 2026-05-25Codex 继续完成低风险模块提取。
- 新增 `slash_actions.rs`,迁出 `SlashActionKind` / `SlashAction` / `SLASH_ACTIONS``lib.rs` 继续持有 slash 命令执行与 Leptos view。
- 新增 `table_commands.rs`,迁出 table toolbar / option action、命令执行和 checked 状态计算。
- 新增 `bridge_dispatch.rs`,迁出 `HostCommandKind` 及 host command payload 解析;真实 command 执行 match 仍留在 `lib.rs`
- 已验证:`cargo fmt --manifest-path rust/Cargo.toml --all --check``cargo check --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml``cargo test --manifest-path rust/spikes/leptos-tiptap-spike/Cargo.toml command -- --test-threads=1`、wasm release build、`wasm-bindgen`、task489、task488、task479、临时 slash 插入简单表格 + 表格行 aux 命令 Playwright 回归。
@@ -1244,47 +1244,47 @@ function __wbg_get_imports() {
}
}, arguments); },
__wbindgen_cast_0000000000000001: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1143, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5);
return ret;
},
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1196, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 1195, ret: Result(Unit), inner_ret: Some(Result(Unit)) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h3a3182d847094e12);
return ret;
},
__wbindgen_cast_0000000000000003: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 972, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [Externref], shim_idx: 971, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__had771ddc65647798);
return ret;
},
__wbindgen_cast_0000000000000004: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1094, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1093, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0abd2b2fe4652e2a);
return ret;
},
__wbindgen_cast_0000000000000005: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1144, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("Event")], shim_idx: 1143, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h396914cf76a9e7a5_4);
return ret;
},
__wbindgen_cast_0000000000000006: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 598, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [NamedExternref("KeyboardEvent")], shim_idx: 879, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__hd90af689bc3e71bf);
return ret;
},
__wbindgen_cast_0000000000000007: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1096, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1095, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h794babeffd4f821f);
return ret;
},
__wbindgen_cast_0000000000000008: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1111, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1110, ret: Unit, inner_ret: Some(Unit) }, mutable: false }) -> Externref`.
const ret = makeClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h2b648e7ac8ec1fb5);
return ret;
},
__wbindgen_cast_0000000000000009: function(arg0, arg1) {
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1147, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
// Cast intrinsic for `Closure(Closure { owned: true, function: Function { arguments: [], shim_idx: 1146, ret: Unit, inner_ret: Some(Unit) }, mutable: true }) -> Externref`.
const ret = makeMutClosure(arg0, arg1, wasm_bindgen__convert__closures_____invoke__h0b7fb40a8610550c);
return ret;
},
@@ -0,0 +1,86 @@
use crate::editor_runtime::bridge_events::HostCommandPayload;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum HostCommandKind {
Undo,
Redo,
ReplaceContent,
SetPageOptions,
InsertInlineReference,
InsertEmbedReference,
RequestCurrentBlockId,
SetEditable,
Focus,
Bootstrap,
}
impl HostCommandKind {
pub(crate) fn from_event_name(event_name: &str) -> Option<Self> {
match event_name {
"undo" => Some(Self::Undo),
"redo" => Some(Self::Redo),
"replaceContent" | "replace-document" => Some(Self::ReplaceContent),
"setPageOptions" | "set-page-options" => Some(Self::SetPageOptions),
"insertInlineReference" => Some(Self::InsertInlineReference),
"insertEmbedReference" => Some(Self::InsertEmbedReference),
"requestCurrentBlockId" => Some(Self::RequestCurrentBlockId),
"setEditable" | "set-editable" => Some(Self::SetEditable),
"focus" => Some(Self::Focus),
"bootstrap" => Some(Self::Bootstrap),
_ => None,
}
}
pub(crate) fn from_payload(payload: &HostCommandPayload) -> Option<Self> {
payload.command.as_deref().and_then(Self::from_event_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_command_aliases_and_canonical_names() {
assert_eq!(
HostCommandKind::from_event_name("replace-document"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("replaceContent"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("requestCurrentBlockId"),
Some(HostCommandKind::RequestCurrentBlockId)
);
}
#[test]
fn reads_command_from_payload_field() {
let payload = HostCommandPayload {
command: Some("setEditable".to_string()),
document_id: None,
workspace_id: None,
title: None,
content: None,
editable: Some(true),
page_options: None,
block_id: None,
block_index: None,
text: None,
reference_document_id: None,
reference_block_id: None,
current_block_id: None,
selection: None,
revision: None,
conflict_detection_key: None,
read_only: None,
};
assert_eq!(
HostCommandKind::from_payload(&payload),
Some(HostCommandKind::SetEditable)
);
}
}
@@ -4,6 +4,7 @@ pub(crate) mod block_dnd;
pub(crate) mod block_hover_state;
pub(crate) mod block_menu_document;
pub(crate) mod block_menu_overlay;
pub(crate) mod bridge_dispatch;
pub(crate) mod bridge_events;
pub(crate) mod command_sync;
pub(crate) mod content_layout;
@@ -14,3 +15,5 @@ pub(crate) mod history_safe_commands;
pub(crate) mod mindmap_node_view;
pub(crate) mod overlays;
pub(crate) mod persistence;
pub(crate) mod slash_actions;
pub(crate) mod table_commands;
@@ -0,0 +1,237 @@
#[derive(Clone, Copy, PartialEq, Eq)]
pub(crate) enum SlashActionKind {
AiAssistant,
AiWrite,
ContinueWriting,
Summarize,
MoreAi,
Paragraph,
Heading1,
Heading2,
Heading3,
Heading4,
BulletList,
OrderedList,
Todo,
AdvancedTodo,
Quote,
CodeBlock,
Divider,
SimpleTable,
Mindmap,
Image,
UploadAttachment,
Toc,
}
#[derive(Clone, Copy)]
pub(crate) struct SlashAction {
pub(crate) kind: SlashActionKind,
pub(crate) id: &'static str,
pub(crate) category: &'static str,
pub(crate) icon: &'static str,
pub(crate) label: &'static str,
pub(crate) description: &'static str,
pub(crate) shortcut: &'static str,
}
pub(crate) const SLASH_ACTIONS: [SlashAction; 22] = [
SlashAction {
kind: SlashActionKind::AiAssistant,
id: "ai-assistant",
category: "AI 助理",
icon: "",
label: "AI 助理",
description: "按 Wolai 基线保留 AI 入口",
shortcut: "/ai",
},
SlashAction {
kind: SlashActionKind::AiWrite,
id: "ai-write",
category: "AI 助理",
icon: "",
label: "用 AI 写作",
description: "唤起写作辅助入口",
shortcut: "/yaixz",
},
SlashAction {
kind: SlashActionKind::ContinueWriting,
id: "continue-writing",
category: "AI 助理",
icon: "",
label: "续写",
description: "按当前上下文继续写作",
shortcut: "/xx",
},
SlashAction {
kind: SlashActionKind::Summarize,
id: "summarize",
category: "AI 助理",
icon: "",
label: "总结",
description: "对当前内容生成摘要",
shortcut: "/zj",
},
SlashAction {
kind: SlashActionKind::MoreAi,
id: "more-ai",
category: "AI 助理",
icon: "",
label: "更多",
description: "更多 AI 命令",
shortcut: "",
},
SlashAction {
kind: SlashActionKind::Paragraph,
id: "paragraph",
category: "基础块列表",
icon: "Aa",
label: "文本",
description: "普通正文块",
shortcut: "/wb",
},
SlashAction {
kind: SlashActionKind::Todo,
id: "todo",
category: "基础块列表",
icon: "",
label: "待办列表",
description: "创建可勾选任务",
shortcut: "/dblb",
},
SlashAction {
kind: SlashActionKind::AdvancedTodo,
id: "advanced-todo",
category: "基础块列表",
icon: "",
label: "高级待办列表",
description: "保留 Wolai 高级待办入口",
shortcut: "/gjdblb",
},
SlashAction {
kind: SlashActionKind::Heading1,
id: "heading-1",
category: "基础块列表",
icon: "H1",
label: "主标题",
description: "一级标题",
shortcut: "/h1",
},
SlashAction {
kind: SlashActionKind::Heading2,
id: "heading-2",
category: "基础块列表",
icon: "H2",
label: "大标题",
description: "二级标题",
shortcut: "/h2",
},
SlashAction {
kind: SlashActionKind::Heading3,
id: "heading-3",
category: "基础块列表",
icon: "H3",
label: "中标题",
description: "三级标题",
shortcut: "/h3",
},
SlashAction {
kind: SlashActionKind::Heading4,
id: "heading-4",
category: "基础块列表",
icon: "H4",
label: "小标题",
description: "四级标题",
shortcut: "/h4",
},
SlashAction {
kind: SlashActionKind::BulletList,
id: "bullet",
category: "基础块列表",
icon: "",
label: "列表",
description: "记录普通要点",
shortcut: "/lb",
},
SlashAction {
kind: SlashActionKind::OrderedList,
id: "ordered",
category: "基础块列表",
icon: "1.",
label: "数字列表",
description: "记录步骤顺序",
shortcut: "/szlb",
},
SlashAction {
kind: SlashActionKind::Quote,
id: "quote",
category: "基础块列表",
icon: "",
label: "引述文字",
description: "包住注释与摘录",
shortcut: "/ys",
},
SlashAction {
kind: SlashActionKind::CodeBlock,
id: "code-block",
category: "基础块列表",
icon: "<> ",
label: "代码片段",
description: "插入带语义的代码块",
shortcut: "/dm",
},
SlashAction {
kind: SlashActionKind::Divider,
id: "divider",
category: "基础块列表",
icon: "",
label: "分割线",
description: "插入分隔线",
shortcut: "/fgx",
},
SlashAction {
kind: SlashActionKind::SimpleTable,
id: "simple-table",
category: "进阶块列表",
icon: "",
label: "简单表格",
description: "插入基础表格",
shortcut: "/jdbg",
},
SlashAction {
kind: SlashActionKind::Mindmap,
id: "mindmap",
category: "进阶块列表",
icon: "",
label: "思维导图",
description: "插入 kernel projection 导图块",
shortcut: "/dt",
},
SlashAction {
kind: SlashActionKind::Image,
id: "image",
category: "媒体与附件",
icon: "",
label: "图片",
description: "上传并插入图片",
shortcut: "/tp",
},
SlashAction {
kind: SlashActionKind::UploadAttachment,
id: "upload-attachment",
category: "媒体与附件",
icon: "",
label: "上传附件",
description: "上传 Office、PDF 或其他文件",
shortcut: "/fj",
},
SlashAction {
kind: SlashActionKind::Toc,
id: "toc",
category: "进阶块列表",
icon: "",
label: "页面目录",
description: "根据当前标题生成目录",
shortcut: "/toc",
},
];
@@ -0,0 +1,180 @@
use leptos_tiptap::TiptapEditorHandle;
use serde_json::Value;
#[derive(Clone, Copy)]
pub(crate) enum TableToolbarAction {
AddRowAfter,
AddColumnAfter,
DeleteRow,
DeleteColumn,
ClearCell,
DeleteTable,
}
#[derive(Clone, Copy)]
pub(crate) enum TableOptionAction {
ToggleHeaderRow,
ToggleHeaderColumn,
ToggleHiddenBorders,
}
impl TableToolbarAction {
pub(crate) fn id(self) -> &'static str {
match self {
Self::AddRowAfter => "add-row-after",
Self::AddColumnAfter => "add-column-after",
Self::DeleteRow => "delete-row",
Self::DeleteColumn => "delete-column",
Self::ClearCell => "clear-cell",
Self::DeleteTable => "delete-table",
}
}
pub(crate) fn icon(self) -> &'static str {
match self {
Self::AddRowAfter => "+R",
Self::AddColumnAfter => "+C",
Self::DeleteRow => "-R",
Self::DeleteColumn => "-C",
Self::ClearCell => "",
Self::DeleteTable => "×",
}
}
pub(crate) fn title(self) -> &'static str {
match self {
Self::AddRowAfter => "下方插入行",
Self::AddColumnAfter => "右侧插入列",
Self::DeleteRow => "删除当前行",
Self::DeleteColumn => "删除当前列",
Self::ClearCell => "清空当前单元格",
Self::DeleteTable => "删除表格",
}
}
}
pub(crate) const TABLE_TOOLBAR_ACTIONS: [TableToolbarAction; 6] = [
TableToolbarAction::AddRowAfter,
TableToolbarAction::AddColumnAfter,
TableToolbarAction::ClearCell,
TableToolbarAction::DeleteRow,
TableToolbarAction::DeleteColumn,
TableToolbarAction::DeleteTable,
];
impl TableOptionAction {
pub(crate) fn id(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "toggle-header-row",
Self::ToggleHeaderColumn => "toggle-header-column",
Self::ToggleHiddenBorders => "toggle-hidden-borders",
}
}
pub(crate) fn title(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "标题行",
Self::ToggleHeaderColumn => "标题列",
Self::ToggleHiddenBorders => "隐藏边框线",
}
}
}
pub(crate) const TABLE_OPTION_ACTIONS: [TableOptionAction; 3] = [
TableOptionAction::ToggleHeaderRow,
TableOptionAction::ToggleHeaderColumn,
TableOptionAction::ToggleHiddenBorders,
];
pub(crate) fn run_table_toolbar_action(
editor: TiptapEditorHandle,
action: TableToolbarAction,
) -> Result<&'static str, String> {
let result = match action {
TableToolbarAction::AddRowAfter => editor.add_table_row_after(),
TableToolbarAction::AddColumnAfter => editor.add_table_column_after(),
TableToolbarAction::DeleteRow => editor.delete_table_row(),
TableToolbarAction::DeleteColumn => editor.delete_table_column(),
TableToolbarAction::ClearCell => editor.clear_table_cell(),
TableToolbarAction::DeleteTable => editor.delete_table(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
pub(crate) fn run_table_option_action(
editor: TiptapEditorHandle,
action: TableOptionAction,
) -> Result<&'static str, String> {
let result = match action {
TableOptionAction::ToggleHeaderRow => editor.toggle_table_header_row(),
TableOptionAction::ToggleHeaderColumn => editor.toggle_table_header_column(),
TableOptionAction::ToggleHiddenBorders => editor.toggle_table_hidden_borders(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
fn first_table_node(value: &Value) -> Option<&Value> {
if value.get("type").and_then(Value::as_str) == Some("table") {
return Some(value);
}
if let Some(table) = value
.get("props")
.and_then(|props| props.get("tiptapTable"))
.and_then(first_table_node)
{
return Some(table);
}
for key in ["content", "children"] {
if let Some(children) = value.get(key).and_then(Value::as_array) {
if let Some(table) = children.iter().find_map(first_table_node) {
return Some(table);
}
}
}
None
}
pub(crate) fn table_option_checked(document: &Value, action: TableOptionAction) -> bool {
let Some(table) = first_table_node(document) else {
return false;
};
match action {
TableOptionAction::ToggleHiddenBorders => table
.get("attrs")
.and_then(|attrs| attrs.get("hiddenBorders"))
.and_then(Value::as_bool)
.unwrap_or(false),
TableOptionAction::ToggleHeaderRow => table
.get("content")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("content"))
.and_then(Value::as_array)
.map(|cells| {
!cells.is_empty()
&& cells
.iter()
.all(|cell| cell.get("type").and_then(Value::as_str) == Some("tableHeader"))
})
.unwrap_or(false),
TableOptionAction::ToggleHeaderColumn => table
.get("content")
.and_then(Value::as_array)
.map(|rows| {
!rows.is_empty()
&& rows.iter().all(|row| {
row.get("content")
.and_then(Value::as_array)
.and_then(|cells| cells.first())
.and_then(|cell| cell.get("type"))
.and_then(Value::as_str)
== Some("tableHeader")
})
})
.unwrap_or(false),
}
}
+6 -496
View File
@@ -34,6 +34,7 @@ use editor_runtime::block_menu_document::{
use editor_runtime::block_hover_state::{
BlockMenuLayout, DropIndicatorState, HoveredBlockState, PendingDragState,
};
use editor_runtime::bridge_dispatch::HostCommandKind;
use editor_runtime::bridge_events::{
BridgeEnvelope, BridgeSelectorsPayload, ChangeMetaPayload, ChangePayload, HeightPayload,
HostCommandEnvelope, HostCommandPayload, HostStatusPayload, ReadyPayload, RuntimePageOptions,
@@ -70,6 +71,11 @@ use editor_runtime::persistence::{
load_persisted_document, normalize_identity_value, persist_document_state,
persisted_document_identity, PersistedDocumentIdentity,
};
use editor_runtime::slash_actions::{SlashActionKind, SLASH_ACTIONS};
use editor_runtime::table_commands::{
run_table_option_action, run_table_toolbar_action, table_option_checked, TableToolbarAction,
TABLE_OPTION_ACTIONS, TABLE_TOOLBAR_ACTIONS,
};
#[cfg(test)]
use editor_runtime::persistence::persisted_document_storage_key;
@@ -2749,43 +2755,6 @@ const SPIKE_STYLE: &str = r#"
}
"#;
#[derive(Clone, Copy, PartialEq, Eq)]
enum SlashActionKind {
AiAssistant,
AiWrite,
ContinueWriting,
Summarize,
MoreAi,
Paragraph,
Heading1,
Heading2,
Heading3,
Heading4,
BulletList,
OrderedList,
Todo,
AdvancedTodo,
Quote,
CodeBlock,
Divider,
SimpleTable,
Mindmap,
Image,
UploadAttachment,
Toc,
}
#[derive(Clone, Copy)]
struct SlashAction {
kind: SlashActionKind,
id: &'static str,
category: &'static str,
icon: &'static str,
label: &'static str,
description: &'static str,
shortcut: &'static str,
}
#[derive(Clone, Copy)]
struct FoldedHeadingAction {
level: u8,
@@ -2821,207 +2790,6 @@ const FOLDED_HEADING_ACTIONS: [FoldedHeadingAction; 4] = [
},
];
const SLASH_ACTIONS: [SlashAction; 22] = [
SlashAction {
kind: SlashActionKind::AiAssistant,
id: "ai-assistant",
category: "AI 助理",
icon: "",
label: "AI 助理",
description: "按 Wolai 基线保留 AI 入口",
shortcut: "/ai",
},
SlashAction {
kind: SlashActionKind::AiWrite,
id: "ai-write",
category: "AI 助理",
icon: "",
label: "用 AI 写作",
description: "唤起写作辅助入口",
shortcut: "/yaixz",
},
SlashAction {
kind: SlashActionKind::ContinueWriting,
id: "continue-writing",
category: "AI 助理",
icon: "",
label: "续写",
description: "按当前上下文继续写作",
shortcut: "/xx",
},
SlashAction {
kind: SlashActionKind::Summarize,
id: "summarize",
category: "AI 助理",
icon: "",
label: "总结",
description: "对当前内容生成摘要",
shortcut: "/zj",
},
SlashAction {
kind: SlashActionKind::MoreAi,
id: "more-ai",
category: "AI 助理",
icon: "",
label: "更多",
description: "更多 AI 命令",
shortcut: "",
},
SlashAction {
kind: SlashActionKind::Paragraph,
id: "paragraph",
category: "基础块列表",
icon: "Aa",
label: "文本",
description: "普通正文块",
shortcut: "/wb",
},
SlashAction {
kind: SlashActionKind::Todo,
id: "todo",
category: "基础块列表",
icon: "",
label: "待办列表",
description: "创建可勾选任务",
shortcut: "/dblb",
},
SlashAction {
kind: SlashActionKind::AdvancedTodo,
id: "advanced-todo",
category: "基础块列表",
icon: "",
label: "高级待办列表",
description: "保留 Wolai 高级待办入口",
shortcut: "/gjdblb",
},
SlashAction {
kind: SlashActionKind::Heading1,
id: "heading-1",
category: "基础块列表",
icon: "H1",
label: "主标题",
description: "一级标题",
shortcut: "/h1",
},
SlashAction {
kind: SlashActionKind::Heading2,
id: "heading-2",
category: "基础块列表",
icon: "H2",
label: "大标题",
description: "二级标题",
shortcut: "/h2",
},
SlashAction {
kind: SlashActionKind::Heading3,
id: "heading-3",
category: "基础块列表",
icon: "H3",
label: "中标题",
description: "三级标题",
shortcut: "/h3",
},
SlashAction {
kind: SlashActionKind::Heading4,
id: "heading-4",
category: "基础块列表",
icon: "H4",
label: "小标题",
description: "四级标题",
shortcut: "/h4",
},
SlashAction {
kind: SlashActionKind::BulletList,
id: "bullet",
category: "基础块列表",
icon: "",
label: "列表",
description: "记录普通要点",
shortcut: "/lb",
},
SlashAction {
kind: SlashActionKind::OrderedList,
id: "ordered",
category: "基础块列表",
icon: "1.",
label: "数字列表",
description: "记录步骤顺序",
shortcut: "/szlb",
},
SlashAction {
kind: SlashActionKind::Quote,
id: "quote",
category: "基础块列表",
icon: "",
label: "引述文字",
description: "包住注释与摘录",
shortcut: "/ys",
},
SlashAction {
kind: SlashActionKind::CodeBlock,
id: "code-block",
category: "基础块列表",
icon: "<> ",
label: "代码片段",
description: "插入带语义的代码块",
shortcut: "/dm",
},
SlashAction {
kind: SlashActionKind::Divider,
id: "divider",
category: "基础块列表",
icon: "",
label: "分割线",
description: "插入分隔线",
shortcut: "/fgx",
},
SlashAction {
kind: SlashActionKind::SimpleTable,
id: "simple-table",
category: "进阶块列表",
icon: "",
label: "简单表格",
description: "插入基础表格",
shortcut: "/jdbg",
},
SlashAction {
kind: SlashActionKind::Mindmap,
id: "mindmap",
category: "进阶块列表",
icon: "",
label: "思维导图",
description: "插入 kernel projection 导图块",
shortcut: "/dt",
},
SlashAction {
kind: SlashActionKind::Image,
id: "image",
category: "媒体与附件",
icon: "",
label: "图片",
description: "上传并插入图片",
shortcut: "/tp",
},
SlashAction {
kind: SlashActionKind::UploadAttachment,
id: "upload-attachment",
category: "媒体与附件",
icon: "",
label: "上传附件",
description: "上传 Office、PDF 或其他文件",
shortcut: "/fj",
},
SlashAction {
kind: SlashActionKind::Toc,
id: "toc",
category: "进阶块列表",
icon: "",
label: "页面目录",
description: "根据当前标题生成目录",
shortcut: "/toc",
},
];
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct LegacyHostEnvelope {
@@ -3045,90 +2813,10 @@ struct HostDocumentPayload {
read_only: Option<bool>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum HostCommandKind {
Undo,
Redo,
ReplaceContent,
SetPageOptions,
InsertInlineReference,
InsertEmbedReference,
RequestCurrentBlockId,
SetEditable,
Focus,
Bootstrap,
}
impl HostCommandKind {
fn from_event_name(event_name: &str) -> Option<Self> {
match event_name {
"undo" => Some(Self::Undo),
"redo" => Some(Self::Redo),
"replaceContent" | "replace-document" => Some(Self::ReplaceContent),
"setPageOptions" | "set-page-options" => Some(Self::SetPageOptions),
"insertInlineReference" => Some(Self::InsertInlineReference),
"insertEmbedReference" => Some(Self::InsertEmbedReference),
"requestCurrentBlockId" => Some(Self::RequestCurrentBlockId),
"setEditable" | "set-editable" => Some(Self::SetEditable),
"focus" => Some(Self::Focus),
"bootstrap" => Some(Self::Bootstrap),
_ => None,
}
}
fn from_payload(payload: &HostCommandPayload) -> Option<Self> {
payload.command.as_deref().and_then(Self::from_event_name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_command_aliases_and_canonical_names() {
assert_eq!(
HostCommandKind::from_event_name("replace-document"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("replaceContent"),
Some(HostCommandKind::ReplaceContent)
);
assert_eq!(
HostCommandKind::from_event_name("requestCurrentBlockId"),
Some(HostCommandKind::RequestCurrentBlockId)
);
}
#[test]
fn reads_command_from_payload_field() {
let payload = HostCommandPayload {
command: Some("setEditable".to_string()),
document_id: None,
workspace_id: None,
title: None,
content: None,
editable: Some(true),
page_options: None,
block_id: None,
block_index: None,
text: None,
reference_document_id: None,
reference_block_id: None,
current_block_id: None,
selection: None,
revision: None,
conflict_detection_key: None,
read_only: None,
};
assert_eq!(
HostCommandKind::from_payload(&payload),
Some(HostCommandKind::SetEditable)
);
}
#[test]
fn resolves_embedded_mode_from_explicit_runtime_mode_first() {
assert_eq!(
@@ -4292,184 +3980,6 @@ fn apply_image_align(
.map_err(|err| format!("命令执行失败:{err}"))
}
#[derive(Clone, Copy)]
enum TableToolbarAction {
AddRowAfter,
AddColumnAfter,
DeleteRow,
DeleteColumn,
ClearCell,
DeleteTable,
}
#[derive(Clone, Copy)]
enum TableOptionAction {
ToggleHeaderRow,
ToggleHeaderColumn,
ToggleHiddenBorders,
}
impl TableToolbarAction {
fn id(self) -> &'static str {
match self {
Self::AddRowAfter => "add-row-after",
Self::AddColumnAfter => "add-column-after",
Self::DeleteRow => "delete-row",
Self::DeleteColumn => "delete-column",
Self::ClearCell => "clear-cell",
Self::DeleteTable => "delete-table",
}
}
fn icon(self) -> &'static str {
match self {
Self::AddRowAfter => "+R",
Self::AddColumnAfter => "+C",
Self::DeleteRow => "-R",
Self::DeleteColumn => "-C",
Self::ClearCell => "",
Self::DeleteTable => "×",
}
}
fn title(self) -> &'static str {
match self {
Self::AddRowAfter => "下方插入行",
Self::AddColumnAfter => "右侧插入列",
Self::DeleteRow => "删除当前行",
Self::DeleteColumn => "删除当前列",
Self::ClearCell => "清空当前单元格",
Self::DeleteTable => "删除表格",
}
}
}
const TABLE_TOOLBAR_ACTIONS: [TableToolbarAction; 6] = [
TableToolbarAction::AddRowAfter,
TableToolbarAction::AddColumnAfter,
TableToolbarAction::ClearCell,
TableToolbarAction::DeleteRow,
TableToolbarAction::DeleteColumn,
TableToolbarAction::DeleteTable,
];
impl TableOptionAction {
fn id(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "toggle-header-row",
Self::ToggleHeaderColumn => "toggle-header-column",
Self::ToggleHiddenBorders => "toggle-hidden-borders",
}
}
fn title(self) -> &'static str {
match self {
Self::ToggleHeaderRow => "标题行",
Self::ToggleHeaderColumn => "标题列",
Self::ToggleHiddenBorders => "隐藏边框线",
}
}
}
const TABLE_OPTION_ACTIONS: [TableOptionAction; 3] = [
TableOptionAction::ToggleHeaderRow,
TableOptionAction::ToggleHeaderColumn,
TableOptionAction::ToggleHiddenBorders,
];
fn run_table_toolbar_action(
editor: TiptapEditorHandle,
action: TableToolbarAction,
) -> Result<&'static str, String> {
let result = match action {
TableToolbarAction::AddRowAfter => editor.add_table_row_after(),
TableToolbarAction::AddColumnAfter => editor.add_table_column_after(),
TableToolbarAction::DeleteRow => editor.delete_table_row(),
TableToolbarAction::DeleteColumn => editor.delete_table_column(),
TableToolbarAction::ClearCell => editor.clear_table_cell(),
TableToolbarAction::DeleteTable => editor.delete_table(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
fn run_table_option_action(
editor: TiptapEditorHandle,
action: TableOptionAction,
) -> Result<&'static str, String> {
let result = match action {
TableOptionAction::ToggleHeaderRow => editor.toggle_table_header_row(),
TableOptionAction::ToggleHeaderColumn => editor.toggle_table_header_column(),
TableOptionAction::ToggleHiddenBorders => editor.toggle_table_hidden_borders(),
};
result
.map(|_| action.title())
.map_err(|err| format!("命令执行失败:{err}"))
}
fn first_table_node(value: &Value) -> Option<&Value> {
if value.get("type").and_then(Value::as_str) == Some("table") {
return Some(value);
}
if let Some(table) = value
.get("props")
.and_then(|props| props.get("tiptapTable"))
.and_then(first_table_node)
{
return Some(table);
}
for key in ["content", "children"] {
if let Some(children) = value.get(key).and_then(Value::as_array) {
if let Some(table) = children.iter().find_map(first_table_node) {
return Some(table);
}
}
}
None
}
fn table_option_checked(document: &Value, action: TableOptionAction) -> bool {
let Some(table) = first_table_node(document) else {
return false;
};
match action {
TableOptionAction::ToggleHiddenBorders => table
.get("attrs")
.and_then(|attrs| attrs.get("hiddenBorders"))
.and_then(Value::as_bool)
.unwrap_or(false),
TableOptionAction::ToggleHeaderRow => table
.get("content")
.and_then(Value::as_array)
.and_then(|rows| rows.first())
.and_then(|row| row.get("content"))
.and_then(Value::as_array)
.map(|cells| {
!cells.is_empty()
&& cells
.iter()
.all(|cell| cell.get("type").and_then(Value::as_str) == Some("tableHeader"))
})
.unwrap_or(false),
TableOptionAction::ToggleHeaderColumn => table
.get("content")
.and_then(Value::as_array)
.map(|rows| {
!rows.is_empty()
&& rows.iter().all(|row| {
row.get("content")
.and_then(Value::as_array)
.and_then(|cells| cells.first())
.and_then(|cell| cell.get("type"))
.and_then(Value::as_str)
== Some("tableHeader")
})
})
.unwrap_or(false),
}
}
fn run_slash_action(
editor: TiptapEditorHandle,
action: SlashActionKind,