1use std::ops::Range;
4use std::sync::Arc;
5
6use rich::measure::Measurement;
7use rich::{AnsiDecoder, Console, ConsoleOptions, Renderable, Segment, Style, Text, Theme};
8
9use super::engine::{
10 diff_lines, group_ranges, hunk_header, split_lines_inclusive, strip_eol, Op, TextDiff,
11};
12use super::render::{self, Block, Kind, Options, Row};
13use super::{style, Layout};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub enum Side {
18 Old,
19 New,
20}
21
22pub(crate) type LineLinks = Arc<dyn Fn(Side, usize) -> Option<String> + Send + Sync>;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26pub(crate) enum Tag {
27 Equal,
28 Delete,
29 Insert,
30 Style,
32}
33
34#[derive(Clone)]
46pub struct DiffView {
47 old: Vec<Text>,
48 new: Vec<Text>,
49 chunks: Vec<(Tag, Range<usize>, Range<usize>)>,
50 old_no_newline: bool,
51 new_no_newline: bool,
52 layout: Layout,
53 line_numbers: bool,
54 wrap: bool,
55 context: usize,
56 titles: Option<(String, String)>,
57 emphasis: bool,
58 links: Option<LineLinks>,
59}
60
61impl std::fmt::Debug for DiffView {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 f.debug_struct("DiffView")
64 .field("old_lines", &self.old.len())
65 .field("new_lines", &self.new.len())
66 .field("layout", &self.layout)
67 .field("context", &self.context)
68 .finish_non_exhaustive()
69 }
70}
71
72#[cfg_attr(not(feature = "syntax"), allow(dead_code))]
74pub(crate) fn text_lines(text: &Text) -> Vec<Text> {
75 let plain = text.plain();
76 let offsets: Vec<usize> = plain
77 .match_indices('\n')
78 .map(|(i, _)| i + 1)
79 .filter(|&i| i < plain.len())
80 .collect();
81 if plain.is_empty() {
82 return Vec::new();
83 }
84 text.divide(&offsets)
85 .into_iter()
86 .map(|mut line| {
87 if line.plain().ends_with('\n') {
88 line.right_crop(1);
89 }
90 if line.plain().ends_with('\r') {
91 line.right_crop(1);
92 }
93 line
94 })
95 .collect()
96}
97
98fn signature(text: &Text) -> Vec<Segment> {
100 let segments = text.render(&Theme::default_theme(), &Style::new());
101 Segment::simplify(
102 &segments
103 .into_iter()
104 .filter(|s| !s.text.is_empty())
105 .collect::<Vec<_>>(),
106 )
107}
108
109impl DiffView {
110 pub fn new(old: &str, new: &str) -> Self {
112 let plain = |s: &str| {
113 split_lines_inclusive(s)
114 .into_iter()
115 .map(|l| Text::new(strip_eol(l)))
116 .collect()
117 };
118 Self::from_parts(
119 plain(old),
120 plain(new),
121 &split_lines_inclusive(old),
122 &split_lines_inclusive(new),
123 false,
124 )
125 }
126
127 pub fn from_diff(diff: &TextDiff) -> Self {
129 let lines = |ls: &[String]| ls.iter().map(|l| Text::new(strip_eol(l))).collect();
130 let old: Vec<&str> = diff.old_lines().iter().map(String::as_str).collect();
131 let new: Vec<&str> = diff.new_lines().iter().map(String::as_str).collect();
132 let mut view = Self::from_ops(
133 lines(diff.old_lines()),
134 lines(diff.new_lines()),
135 diff.ops(),
136 &old,
137 &new,
138 );
139 view.context = diff.context_lines();
140 view
141 }
142
143 pub fn ansi(old: &str, new: &str) -> Self {
147 let decode = |s: &str| {
148 let mut decoder = AnsiDecoder::new();
149 let lines = split_lines_inclusive(s);
150 let texts: Vec<Text> = lines
151 .iter()
152 .map(|l| decoder.decode_line(strip_eol(l)))
153 .collect();
154 let keys: Vec<String> = lines
156 .iter()
157 .zip(&texts)
158 .map(|(l, t)| {
159 let mut key = t.plain().to_string();
160 if l.ends_with('\n') {
161 key.push('\n');
162 }
163 key
164 })
165 .collect();
166 (texts, keys)
167 };
168 let (old_texts, old_keys) = decode(old);
169 let (new_texts, new_keys) = decode(new);
170 let ok: Vec<&str> = old_keys.iter().map(String::as_str).collect();
171 let nk: Vec<&str> = new_keys.iter().map(String::as_str).collect();
172 Self::from_parts(old_texts, new_texts, &ok, &nk, true)
173 }
174
175 #[cfg(feature = "testing")]
178 pub fn snapshots(
179 old: &crate::testing::RenderSnapshot,
180 new: &crate::testing::RenderSnapshot,
181 ) -> Self {
182 Self::ansi(&old.ansi, &new.ansi)
183 }
184
185 pub(crate) fn from_parts(
187 old: Vec<Text>,
188 new: Vec<Text>,
189 old_keys: &[&str],
190 new_keys: &[&str],
191 styles: bool,
192 ) -> Self {
193 let ops = diff_lines(old_keys, new_keys);
194 let mut view = Self::from_ops(old, new, &ops, old_keys, new_keys);
195 if styles {
196 view.detect_style_changes();
197 }
198 view
199 }
200
201 fn from_ops(
202 old: Vec<Text>,
203 new: Vec<Text>,
204 ops: &[Op],
205 old_keys: &[&str],
206 new_keys: &[&str],
207 ) -> Self {
208 let chunks = ops
209 .iter()
210 .map(|op| {
211 let tag = match op {
212 Op::Equal { .. } => Tag::Equal,
213 Op::Delete { .. } => Tag::Delete,
214 Op::Insert { .. } => Tag::Insert,
215 };
216 (tag, op.old(), op.new_range())
217 })
218 .collect();
219 let lacks = |keys: &[&str]| keys.last().is_some_and(|l| !l.ends_with('\n'));
220 DiffView {
221 old,
222 new,
223 chunks,
224 old_no_newline: lacks(old_keys),
225 new_no_newline: lacks(new_keys),
226 layout: Layout::Unified,
227 line_numbers: true,
228 wrap: true,
229 context: 3,
230 titles: None,
231 emphasis: true,
232 links: None,
233 }
234 }
235
236 fn detect_style_changes(&mut self) {
238 let mut chunks = Vec::new();
239 for (tag, old, new) in std::mem::take(&mut self.chunks) {
240 if tag != Tag::Equal {
241 chunks.push((tag, old, new));
242 continue;
243 }
244 for (i, j) in old.clone().zip(new.clone()) {
245 let t = if signature(&self.old[i]) == signature(&self.new[j]) {
246 Tag::Equal
247 } else {
248 Tag::Style
249 };
250 match chunks.last_mut() {
251 Some((last, a, b)) if *last == t && a.end == i && b.end == j => {
252 a.end = i + 1;
253 b.end = j + 1;
254 }
255 _ => chunks.push((t, i..i + 1, j..j + 1)),
256 }
257 }
258 }
259 self.chunks = chunks;
260 }
261
262 pub fn layout(mut self, layout: Layout) -> Self {
264 self.layout = layout;
265 self
266 }
267 pub fn line_numbers(mut self, show: bool) -> Self {
269 self.line_numbers = show;
270 self
271 }
272 pub fn wrap(mut self, wrap: bool) -> Self {
274 self.wrap = wrap;
275 self
276 }
277 pub fn context(mut self, lines: usize) -> Self {
279 self.context = lines;
280 self
281 }
282 pub fn titles(mut self, old: impl Into<String>, new: impl Into<String>) -> Self {
285 self.titles = Some((old.into(), new.into()));
286 self
287 }
288 pub fn emphasis(mut self, on: bool) -> Self {
290 self.emphasis = on;
291 self
292 }
293 pub(crate) fn links(mut self, links: Option<LineLinks>) -> Self {
294 self.links = links;
295 self
296 }
297
298 pub fn is_equal(&self) -> bool {
300 self.chunks.iter().all(|c| c.0 == Tag::Equal)
301 }
302
303 pub fn stats(&self) -> (usize, usize, usize) {
305 self.chunks
306 .iter()
307 .fold((0, 0, 0), |(a, r, s), (tag, o, n)| match tag {
308 Tag::Insert => (a + n.len(), r, s),
309 Tag::Delete => (a, r + o.len(), s),
310 Tag::Style => (a, r, s + o.len()),
311 Tag::Equal => (a, r, s),
312 })
313 }
314
315 pub fn style_changed_lines(&self) -> Vec<usize> {
317 self.chunks
318 .iter()
319 .filter(|c| c.0 == Tag::Style)
320 .flat_map(|c| c.2.clone().map(|j| j + 1))
321 .collect()
322 }
323
324 pub(crate) fn blocks(&self) -> Vec<Block> {
325 let runs: Vec<(bool, Range<usize>, Range<usize>)> = self
326 .chunks
327 .iter()
328 .map(|(t, o, n)| (*t == Tag::Equal, o.clone(), n.clone()))
329 .collect();
330 let link = |side: Side, n: usize| self.links.as_ref().and_then(|f| f(side, n));
331 let mut blocks = Vec::new();
332 for group in group_ranges(&runs, self.context) {
333 let old_range =
334 group.first().map_or(0, |g| g.1.start)..group.last().map_or(0, |g| g.1.end);
335 let new_range =
336 group.first().map_or(0, |g| g.2.start)..group.last().map_or(0, |g| g.2.end);
337 let mut block = Block {
338 header: Some(hunk_header(old_range, new_range)),
339 rows: Vec::new(),
340 };
341 for (index, old, new) in group {
342 let old_row = |i: usize, kind: Kind| {
343 let mut row = Row::new(kind, Some(i + 1), None, self.old[i].clone());
344 row.no_newline = self.old_no_newline && i + 1 == self.old.len();
345 row.link = link(Side::Old, i + 1);
346 row
347 };
348 let new_row = |j: usize, kind: Kind| {
349 let mut row = Row::new(kind, None, Some(j + 1), self.new[j].clone());
350 row.no_newline = self.new_no_newline && j + 1 == self.new.len();
351 row.link = link(Side::New, j + 1);
352 row
353 };
354 match self.chunks[index].0 {
355 Tag::Equal => {
356 for (i, j) in old.zip(new) {
357 let mut row = new_row(j, Kind::Context);
358 row.old_no = Some(i + 1);
359 block.rows.push(row);
360 }
361 }
362 Tag::Delete => block.rows.extend(old.map(|i| old_row(i, Kind::Delete))),
363 Tag::Insert => block.rows.extend(new.map(|j| new_row(j, Kind::Insert))),
364 Tag::Style => {
365 for (i, j) in old.zip(new) {
366 block.rows.push(old_row(i, Kind::StyleOld));
367 block.rows.push(new_row(j, Kind::StyleNew));
368 }
369 }
370 }
371 }
372 if self.emphasis {
373 render::emphasize(&mut block);
374 }
375 blocks.push(block);
376 }
377 blocks
378 }
379
380 fn options(&self) -> Options {
381 Options {
382 layout: self.layout,
383 line_numbers: self.line_numbers,
384 wrap: self.wrap,
385 titles: self.titles.clone(),
386 }
387 }
388}
389
390impl Renderable for DiffView {
391 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
392 if options.max_width == 0 || options.height == Some(0) {
393 return Vec::new();
394 }
395 let blocks = self.blocks();
396 if blocks.is_empty() {
397 let rows = render::banner(
398 "no differences",
399 style(console, "diff.line_number"),
400 options.max_width,
401 );
402 return render::join(rows, options.height);
403 }
404 let rows = render::render(console, &blocks, &self.options(), options.max_width);
405 render::join(rows, options.height)
406 }
407
408 fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
409 let blocks = self.blocks();
410 let (min, max) = if blocks.is_empty() {
411 (1, "no differences".len())
412 } else {
413 render::measure(&blocks, &self.options())
414 };
415 let max = max.min(options.max_width);
416 Measurement::new(min.min(max), max)
417 }
418}