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