Skip to content

Commit e1cbb3f

Browse files
committed
feat(refine): diff_markdown_ops 暴露整篇有序行级 diff 流
1 parent f0f1f50 commit e1cbb3f

3 files changed

Lines changed: 100 additions & 1 deletion

File tree

‎app/src-tauri/src/commands_notes_version.rs‎

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tauri::State;
1111
use crate::commands::AppState;
1212
use crate::db_ai_usage::AiUsageRecord;
1313
use crate::db_notes_versions::NoteVersion;
14-
use crate::note_diff::{diff_markdown, diff_sections, DiffOp, SectionDiff};
14+
use crate::note_diff::{diff_markdown, diff_sections, diff_stats, DiffOp, SectionDiff};
1515
use crate::types::Note;
1616

1717
/// 版本列表(旧→新;含惰性首快照——旧数据迁移兼容)。
@@ -80,6 +80,31 @@ pub fn diff_markdown_sections(old_md: String, new_md: String) -> Vec<SectionDiff
8080
diff_sections(&old_md, &new_md)
8181
}
8282

83+
/// 整篇有序行级 diff 响应(diff_markdown_ops 数据源——三态流 + 行数统计)。
84+
#[derive(Debug, Clone, serde::Serialize)]
85+
#[serde(rename_all = "camelCase")]
86+
pub struct MarkdownDiffOps {
87+
/// 有序三态流(base/refined 各行全集划分——LCS 保序,前端按栏消费染色)
88+
pub ops: Vec<DiffOp>,
89+
pub added: usize,
90+
pub removed: usize,
91+
}
92+
93+
/// 任意两篇 markdown 的整篇有序行级 diff(精修工作台行级标色/差异模式)。
94+
///
95+
/// @ai-context Why(批 3 / 用户问题11):工作台此前只有章节分组
96+
/// diff_markdown_sections(丢失行间顺序,无法行级染色);
97+
/// note_versions_diff 需 DB 版本 id,只读/内存文本对比不可用——
98+
/// 补薄命令直接暴露 note_diff::diff_markdown 全文档流(纯函数
99+
/// 复用,无第二套 diff 引擎);前端三入口(会话级/笔记级/只读)
100+
/// 统一经本命令对"实际展示的两版文本"取数,保证行与渲染对齐。
101+
#[tauri::command]
102+
pub fn diff_markdown_ops(old_md: String, new_md: String) -> MarkdownDiffOps {
103+
let ops = diff_markdown(&old_md, &new_md);
104+
let (added, removed, _) = diff_stats(&ops);
105+
MarkdownDiffOps { ops, added, removed }
106+
}
107+
83108
/// 读版本并校验归属(diff 输入防御)。
84109
fn get_version_for_note(state: &AppState, note_id: i64, version_id: i64) -> Result<NoteVersion, String> {
85110
let v = state
@@ -92,3 +117,66 @@ fn get_version_for_note(state: &AppState, note_id: i64, version_id: i64) -> Resu
92117
}
93118
Ok(v)
94119
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::*;
124+
125+
/// AAA:整篇有序流——前后未变行保序、中段删除/新增按原始出现序交错
126+
/// (工作台染色按流消费即得各栏行序,无需二次对齐)。
127+
#[test]
128+
fn diff_markdown_ops_keeps_whole_document_order() {
129+
// Arrange:首行未变、中段一删一增、尾行未变
130+
let old_md = "甲\n删我\n尾".to_string();
131+
let new_md = "甲\n加我\n尾".to_string();
132+
// Act
133+
let out = diff_markdown_ops(old_md, new_md);
134+
// Assert:三态各自一个且有序(unchanged → removed → added)
135+
assert_eq!(
136+
out.ops,
137+
vec![
138+
DiffOp::Unchanged("甲".into()),
139+
DiffOp::Removed("删我".into()),
140+
DiffOp::Added("加我".into()),
141+
DiffOp::Unchanged("尾".into()),
142+
]
143+
);
144+
assert_eq!((out.added, out.removed), (1, 1));
145+
}
146+
147+
/// AAA:空侧边界——单侧全量标记,另一侧无行。
148+
#[test]
149+
fn diff_markdown_ops_empty_side_counts_every_line() {
150+
// Arrange
151+
let out = diff_markdown_ops(String::new(), "新增1\n新增2".to_string());
152+
// Act/Assert:空基线的整篇都是新增
153+
assert_eq!(out.ops.len(), 2);
154+
assert!(out.ops.iter().all(|o| matches!(o, DiffOp::Added(_))));
155+
assert_eq!((out.added, out.removed), (2, 0));
156+
157+
let out = diff_markdown_ops("删1\n删2".to_string(), String::new());
158+
// Assert:空精修版的整篇都是删除
159+
assert!(out.ops.iter().all(|o| matches!(o, DiffOp::Removed(_))));
160+
assert_eq!((out.added, out.removed), (0, 2));
161+
}
162+
163+
/// AAA:serde 契约——顶层 camelCase + DiffOp 外部小写标签
164+
/// (与前端 types/notes.ts DiffOp 联合类型逐字对齐)。
165+
#[test]
166+
fn diff_markdown_ops_serializes_camel_case_and_lowercase_tags() {
167+
// Arrange/Act
168+
let out = diff_markdown_ops("旧\n同".to_string(), "新\n同".to_string());
169+
let json = serde_json::to_value(&out).unwrap();
170+
// Assert:顶层键 camelCase(ops/added/removed)
171+
assert!(json.get("ops").is_some());
172+
assert!(json.get("added").is_some());
173+
assert!(json.get("removed").is_some());
174+
assert!(json.get("ops").unwrap().is_array());
175+
// Assert:op 条目为外部小写标签 {removed: "旧"} / {added: "新"} / {unchanged: "同"}
176+
assert_eq!(json["ops"][0]["removed"], "旧");
177+
assert_eq!(json["ops"][1]["added"], "新");
178+
assert_eq!(json["ops"][2]["unchanged"], "同");
179+
assert_eq!(json["added"], 1);
180+
assert_eq!(json["removed"], 1);
181+
}
182+
}

‎app/src-tauri/src/lib.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -934,6 +934,9 @@ pub fn run() {
934934
commands_notes_version::note_versions_usage,
935935
commands_notes_version::note_by_session,
936936
commands_notes_version::diff_markdown_sections,
937+
// 批 3(问题11):整篇有序行级 diff(精修工作台三入口统一取数——
938+
// 章节分组 diff 丢失行间顺序,note_versions_diff 需 DB 版本 id)
939+
commands_notes_version::diff_markdown_ops,
937940
// 结构模型与课后精修(REQ-047/049/050 模型版:下载/状态/精修)
938941
commands_refine::structure_model_download,
939942
commands_refine::structure_model_status,

‎app/src/types/notes.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,14 @@ export interface NoteVersion {
217217
/** 段级 diff 操作(Rust DiffOp:三态外部标签) */
218218
export type DiffOp = { unchanged: string } | { added: string } | { removed: string };
219219

220+
/** 整篇有序行级 diff 响应(Rust MarkdownDiffOps;camelCase——diff_markdown_ops:
221+
* 工作台行级染色/差异模式数据源,ops 按 base/refined 原始出现序交错) */
222+
export interface MarkdownDiffOps {
223+
ops: DiffOp[];
224+
added: number;
225+
removed: number;
226+
}
227+
220228
/** AI 成本记录(Rust AiUsageRecord;camelCase 契约) */
221229
export interface AiUsageRecord {
222230
id: number;

0 commit comments

Comments
 (0)