1use std::collections::HashMap;
4
5use serde::{Deserialize, Serialize};
6
7use crate::algo::{self, Edit};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub enum InlineOp {
12 Equal,
13 Delete,
14 Insert,
15}
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct InlineSegment {
20 pub op: InlineOp,
21 pub text: String,
22}
23
24fn is_cjk(c: char) -> bool {
25 matches!(c as u32,
26 0x3400..=0x4DBF | 0x4E00..=0x9FFF | 0xF900..=0xFAFF | 0x20000..=0x2FFFF
27 | 0x3040..=0x30FF | 0xAC00..=0xD7AF)
28}
29
30pub(crate) fn tokenize(s: &str) -> Vec<&str> {
31 #[derive(PartialEq, Clone, Copy)]
32 enum Class {
33 Word,
34 Space,
35 Single,
36 }
37 let class = |c: char| {
38 if is_cjk(c) {
39 Class::Single
40 } else if c.is_alphanumeric() {
41 Class::Word
42 } else if c.is_whitespace() {
43 Class::Space
44 } else {
45 Class::Single
46 }
47 };
48 let mut out = Vec::new();
49 let mut start: Option<(usize, Class)> = None;
50 for (i, c) in s.char_indices() {
51 let cl = class(c);
52 match start {
53 Some((_, k)) if k == cl && k != Class::Single => {}
54 Some((st, _)) => {
55 out.push(&s[st..i]);
56 start = Some((i, cl));
57 }
58 None => start = Some((i, cl)),
59 }
60 }
61 if let Some((st, _)) = start {
62 out.push(&s[st..]);
63 }
64 out
65}
66
67pub fn inline_diff(old: &str, new: &str) -> Vec<InlineSegment> {
68 let ot = tokenize(old);
69 let nt = tokenize(new);
70 let mut ids: HashMap<&str, u32> = HashMap::new();
71 let oi: Vec<u32> = ot
72 .iter()
73 .map(|s| {
74 let n = ids.len() as u32;
75 *ids.entry(s).or_insert(n)
76 })
77 .collect();
78 let ni: Vec<u32> = nt
79 .iter()
80 .map(|s| {
81 let n = ids.len() as u32;
82 *ids.entry(s).or_insert(n)
83 })
84 .collect();
85 let mut segs: Vec<InlineSegment> = Vec::new();
86 let mut push = |op: InlineOp, t: &str| match segs.last_mut() {
87 Some(l) if l.op == op => l.text.push_str(t),
88 _ => segs.push(InlineSegment {
89 op,
90 text: t.to_string(),
91 }),
92 };
93 let (mut dels, mut inss): (Vec<&str>, Vec<&str>) = (Vec::new(), Vec::new());
95 for e in algo::diff(&oi, &ni) {
96 match e {
97 Edit::Equal(i, _) => {
98 for d in dels.drain(..) {
99 push(InlineOp::Delete, d);
100 }
101 for s in inss.drain(..) {
102 push(InlineOp::Insert, s);
103 }
104 push(InlineOp::Equal, ot[i]);
105 }
106 Edit::Delete(i) => dels.push(ot[i]),
107 Edit::Insert(j) => inss.push(nt[j]),
108 }
109 }
110 for d in dels {
111 push(InlineOp::Delete, d);
112 }
113 for s in inss {
114 push(InlineOp::Insert, s);
115 }
116 segs
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn cjk_by_char() {
125 let segs = inline_diff("今天天气很好", "今天天气很差");
126 let del: Vec<_> = segs.iter().filter(|s| s.op == InlineOp::Delete).collect();
127 assert_eq!(del.len(), 1);
128 assert_eq!(del[0].text, "好");
129 }
130
131 #[test]
132 fn words() {
133 let segs = inline_diff("the quick fox", "the slow fox");
134 assert!(
135 segs.iter()
136 .any(|s| s.op == InlineOp::Delete && s.text == "quick")
137 );
138 assert!(
139 segs.iter()
140 .any(|s| s.op == InlineOp::Insert && s.text == "slow")
141 );
142 }
143}