refactor: extract editor action modules
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user