1use crate::{
4 constants::Side,
5 lines::{byte_len, split_on_newlines, LineNumber},
6 mainfn::FgColor,
7 options::DisplayOptions,
8 parse::syntax::{AtomKind, MatchKind, MatchedPos, TokenKind},
9 positions::SingleLineSpan,
10};
11use owo_colors::{OwoColorize, Style};
12use rustc_hash::FxHashMap;
13use std::cmp::{max, min};
14use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
15
16#[derive(Clone, Copy, Debug)]
17pub enum BackgroundColor {
18 Dark,
19 Light,
20}
21
22impl BackgroundColor {
23 pub fn is_dark(self) -> bool {
24 matches!(self, BackgroundColor::Dark)
25 }
26}
27
28fn substring_by_width(s: &str, start: usize, end: usize) -> &str {
30 if start == end {
31 return &s[0..0];
32 }
33
34 assert!(end > start);
35
36 let mut idx_width_iter = s
37 .char_indices()
38 .scan(0, |w, (idx, ch)| {
39 let before = *w;
40 *w += ch.width().unwrap_or(0);
41 Some((idx, before, *w))
42 })
43 .skip_while(|(_, before, _)| *before < start);
44 let byte_start = idx_width_iter
45 .next()
46 .expect("Expected a width index inside `s`.")
47 .0;
48 match idx_width_iter
49 .skip_while(|(_, _, after)| *after <= end)
50 .next()
51 {
52 Some(byte_end) => &s[byte_start..byte_end.0],
53 None => &s[byte_start..],
54 }
55}
56
57fn substring_by_byte(s: &str, start: usize, end: usize) -> &str {
58 &s[start..end]
59}
60
61fn split_string_by_width(s: &str, max_width: usize, pad: bool) -> Vec<(&str, usize)> {
70 let mut res = vec![];
71 let mut s = s;
72
73 while s.width() > max_width {
74 let l = substring_by_width(s, 0, max_width);
75 let used = l.width();
76 let padding = if pad && used < max_width {
77 1
79 } else {
80 0
81 };
82 res.push((l, padding));
83 s = substring_by_width(s, used, s.width());
84 }
85
86 if res.is_empty() || !s.is_empty() {
87 let padding = if pad { max_width - s.width() } else { 0 };
88 res.push((s, padding));
89 }
90
91 res
92}
93
94fn highlight_missing_style_bug(s: &str) -> String {
95 s.on_purple().to_string()
96}
97
98pub fn split_and_apply(
102 line: &str,
103 max_len: usize,
104 use_color: bool,
105 styles: &[(SingleLineSpan, Style)],
106 side: Side,
107) -> Vec<String> {
108 if styles.is_empty() && !line.trim().is_empty() {
109 return split_string_by_width(line, max_len, matches!(side, Side::Left))
111 .into_iter()
112 .map(|(part, _)| {
113 if use_color {
114 highlight_missing_style_bug(part)
115 } else {
116 part.to_owned()
117 }
118 })
119 .collect();
120 }
121
122 let mut styled_parts = vec![];
123 let mut part_start = 0;
124
125 for (part, pad) in split_string_by_width(line, max_len, matches!(side, Side::Left)) {
126 let mut res = String::with_capacity(part.len() + pad);
127 let mut prev_style_end = 0;
128 for (span, style) in styles {
129 let start_col = span.start_col as usize;
130 let end_col = span.end_col as usize;
131
132 if start_col >= part_start + byte_len(&part) {
134 break;
135 }
136
137 if start_col > part_start && prev_style_end < start_col {
139 let unstyled_start = max(prev_style_end, part_start);
141 res.push_str(substring_by_byte(
142 part,
143 unstyled_start - part_start,
144 start_col - part_start,
145 ));
146 }
147
148 if end_col > part_start {
150 let span_s = substring_by_byte(
151 part,
152 max(0, span.start_col as isize - part_start as isize) as usize,
153 min(byte_len(part), end_col - part_start),
154 );
155 res.push_str(&span_s.style(*style).to_string());
156 }
157 prev_style_end = end_col;
158 }
159
160 if prev_style_end < part_start {
163 prev_style_end = part_start;
164 }
165
166 if prev_style_end < part_start + byte_len(part) {
168 let span_s = substring_by_byte(part, prev_style_end - part_start, byte_len(part));
169 res.push_str(span_s);
170 }
171 res.push_str(&" ".repeat(pad));
172
173 styled_parts.push(res);
174 part_start += byte_len(part);
175 }
176
177 styled_parts
178}
179
180#[allow(unused_variables)]
181pub fn tui_split_and_apply(
182 line: &str,
183 max_len: usize,
184 use_color: bool,
185 styles: &[(SingleLineSpan, Style)],
186 side: Side,
187) -> Vec<(String, FgColor)> {
188 if styles.is_empty() && !line.trim().is_empty() {
189 return split_string_by_width(line, max_len, matches!(side, Side::Left))
191 .into_iter()
192 .map(|(part, _)| (part.to_owned(), FgColor::White))
193 .collect();
194 }
195
196 let mut styled_parts: Vec<(String, FgColor)> = vec![];
197 let mut part_start = 0;
198
199 for (part, pad) in split_string_by_width(line, max_len, matches!(side, Side::Left)) {
200 let mut prev_style_end = 0;
201 for (span, style) in styles {
202 let start_col = span.start_col as usize;
203 let end_col = span.end_col as usize;
204
205 if start_col >= part_start + byte_len(&part) {
207 break;
208 }
209
210 if start_col > part_start && prev_style_end < start_col {
212 let unstyled_start = max(prev_style_end, part_start);
214 let span_s =
215 substring_by_byte(part, unstyled_start - part_start, start_col - part_start);
216 styled_parts.push((span_s.to_owned(), FgColor::White));
217 }
218
219 if end_col > part_start {
221 let span_s = substring_by_byte(
222 part,
223 max(0, span.start_col as isize - part_start as isize) as usize,
224 min(byte_len(part), end_col - part_start),
225 );
226 let styled_span_s = span_s.style(*style).to_string();
227
228 let mut color = FgColor::White;
229 if styled_span_s.starts_with("[91m") {
230 color = FgColor::Red;
231 } else if styled_span_s.starts_with("[92m") {
232 color = FgColor::Green;
233 }
234 styled_parts.push((span_s.to_owned(), color));
235 }
236 prev_style_end = end_col;
237 }
238
239 if prev_style_end < part_start {
242 prev_style_end = part_start;
243 }
244
245 if prev_style_end < part_start + byte_len(part) {
247 let span_s = substring_by_byte(part, prev_style_end - part_start, byte_len(part));
248 styled_parts.push((span_s.to_owned(), FgColor::White));
249 }
250
251 part_start += byte_len(part);
252 }
253
254 styled_parts
255}
256
257fn apply_line(line: &str, styles: &[(SingleLineSpan, Style)]) -> String {
260 if styles.is_empty() && !line.is_empty() {
261 return highlight_missing_style_bug(line);
262 }
263
264 let line_bytes = byte_len(line);
265 let mut res = String::with_capacity(line.len());
266 let mut i = 0;
267 for (span, style) in styles {
268 let start_col = span.start_col as usize;
269 let end_col = span.end_col as usize;
270
271 if start_col >= line_bytes {
274 break;
275 }
276
277 if i < start_col {
279 res.push_str(substring_by_byte(line, i, start_col));
280 }
281
282 let span_s = substring_by_byte(line, start_col, min(line_bytes, end_col));
284 res.push_str(&span_s.style(*style).to_string());
285 i = end_col;
286 }
287
288 if i < line_bytes {
290 let span_s = substring_by_byte(line, i, line_bytes);
291 res.push_str(span_s);
292 }
293 res
294}
295
296fn group_by_line(
297 ranges: &[(SingleLineSpan, Style)],
298) -> FxHashMap<LineNumber, Vec<(SingleLineSpan, Style)>> {
299 let mut ranges_by_line: FxHashMap<_, Vec<_>> = FxHashMap::default();
300 for range in ranges {
301 if let Some(matching_ranges) = ranges_by_line.get_mut(&range.0.line) {
302 (*matching_ranges).push(*range);
303 } else {
304 ranges_by_line.insert(range.0.line, vec![*range]);
305 }
306 }
307
308 ranges_by_line
309}
310
311fn style_lines(lines: &[&str], styles: &[(SingleLineSpan, Style)]) -> Vec<String> {
315 let mut ranges_by_line = group_by_line(styles);
316
317 let mut res = Vec::with_capacity(lines.len());
318 for (i, line) in lines.iter().enumerate() {
319 let mut styled_line = String::with_capacity(line.len());
320 let ranges = ranges_by_line
321 .remove(&(i as u32).into())
322 .unwrap_or_default();
323
324 styled_line.push_str(&apply_line(line, &ranges));
325 styled_line.push('\n');
326 res.push(styled_line);
327 }
328 res
329}
330
331pub fn novel_style(style: Style, is_lhs: bool, background: BackgroundColor) -> Style {
332 if background.is_dark() {
333 if is_lhs {
334 style.bright_red()
335 } else {
336 style.bright_green()
337 }
338 } else if is_lhs {
339 style.red()
340 } else {
341 style.green()
342 }
343}
344
345pub fn color_positions(
346 is_lhs: bool,
347 background: BackgroundColor,
348 syntax_highlight: bool,
349 positions: &[MatchedPos],
350) -> Vec<(SingleLineSpan, Style)> {
351 let mut styles = vec![];
352 for pos in positions {
353 let mut style = Style::new();
354 match pos.kind {
355 MatchKind::UnchangedToken { highlight, .. } => {
356 if syntax_highlight {
357 if let TokenKind::Atom(atom_kind) = highlight {
358 match atom_kind {
359 AtomKind::String => {
360 style = if background.is_dark() {
361 style.bright_magenta()
362 } else {
363 style.magenta()
364 };
365 }
366 AtomKind::Comment => {
367 style = style.italic();
368 style = if background.is_dark() {
369 style.bright_blue()
370 } else {
371 style.blue()
372 };
373 }
374 AtomKind::Keyword | AtomKind::Type => {
375 style = style.bold();
376 }
377 AtomKind::Normal => {}
378 }
379 }
380 }
381 }
382 MatchKind::Novel { highlight, .. } => {
383 style = novel_style(style, is_lhs, background);
384 if syntax_highlight
385 && matches!(
386 highlight,
387 TokenKind::Delimiter
388 | TokenKind::Atom(AtomKind::Keyword)
389 | TokenKind::Atom(AtomKind::Type)
390 )
391 {
392 style = style.bold();
393 }
394 if matches!(highlight, TokenKind::Atom(AtomKind::Comment)) {
395 style = style.italic();
396 }
397 }
398 MatchKind::NovelWord { highlight } => {
399 style = novel_style(style, is_lhs, background).bold().underline();
400 if syntax_highlight && matches!(highlight, TokenKind::Atom(AtomKind::Comment)) {
401 style = style.italic();
402 }
403 }
404 MatchKind::NovelLinePart { highlight, .. } => {
405 style = novel_style(style, is_lhs, background);
406 if syntax_highlight && matches!(highlight, TokenKind::Atom(AtomKind::Comment)) {
407 style = style.italic();
408 }
409 }
410 };
411 styles.push((pos.pos, style));
412 }
413 styles
414}
415
416pub fn apply_colors(
417 s: &str,
418 is_lhs: bool,
419 syntax_highlight: bool,
420 background: BackgroundColor,
421 positions: &[MatchedPos],
422) -> Vec<String> {
423 let styles = color_positions(is_lhs, background, syntax_highlight, positions);
424 let lines = split_on_newlines(s);
425 style_lines(&lines, &styles)
426}
427
428fn apply_header_color(s: &str, use_color: bool, background: BackgroundColor) -> String {
429 if use_color {
430 if background.is_dark() {
431 s.bright_yellow().to_string()
432 } else {
433 s.yellow().to_string()
434 }
435 .bold()
436 .to_string()
437 } else {
438 s.to_string()
439 }
440}
441
442#[allow(unused_variables)]
443pub fn header2(
444 lhs_display_path: &str,
445 rhs_display_path: &str,
446 hunk_num: usize,
447 hunk_total: usize,
448 language_name: &str,
449 display_options: &DisplayOptions,
450) -> String {
451 let divider = if hunk_total == 1 {
452 "".to_owned()
453 } else {
454 format!("{}/{} --- ", hunk_num, hunk_total)
455 };
456 format!("--- {}{}", divider, language_name)
457}
458
459pub fn header(
460 lhs_display_path: &str,
461 rhs_display_path: &str,
462 hunk_num: usize,
463 hunk_total: usize,
464 language_name: &str,
465 display_options: &DisplayOptions,
466) -> String {
467 let divider = if hunk_total == 1 {
468 "".to_owned()
469 } else {
470 format!("{}/{} --- ", hunk_num, hunk_total)
471 };
472
473 let rhs_path_pretty = apply_header_color(
474 rhs_display_path,
475 display_options.use_color,
476 display_options.background_color,
477 );
478 let lhs_path_pretty = apply_header_color(
479 lhs_display_path,
480 display_options.use_color,
481 display_options.background_color,
482 );
483 if hunk_num == 1 && lhs_display_path != rhs_display_path && display_options.in_vcs {
484 let renamed = format!("Renamed {} to {}", lhs_path_pretty, rhs_path_pretty);
485 format!(
486 "{}\n{} --- {}{}",
487 renamed, rhs_path_pretty, divider, language_name
488 )
489 } else {
490 let path_pretty = if rhs_display_path == "/dev/null" {
494 lhs_path_pretty
495 } else {
496 rhs_path_pretty
497 };
498 format!("{} --- {}{}", path_pretty, divider, language_name)
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505 use pretty_assertions::assert_eq;
506
507 #[test]
508 fn split_string_simple() {
509 assert_eq!(
510 split_string_by_width("fooba", 3, true),
511 vec![("foo", 0), ("ba", 1)]
512 );
513 }
514
515 #[test]
516 fn split_string_simple_no_pad() {
517 assert_eq!(
518 split_string_by_width("fooba", 3, false),
519 vec![("foo", 0), ("ba", 0)]
520 );
521 }
522
523 #[test]
524 fn split_string_unicode() {
525 assert_eq!(
526 split_string_by_width("ab📦def", 4, true),
527 vec![("ab📦", 0), ("def", 1)]
528 );
529 }
530
531 #[test]
532 fn split_string_cjk() {
533 assert_eq!(
534 split_string_by_width("一个汉字两列宽", 8, false),
535 vec![("一个汉字", 0), ("两列宽", 0)]
536 );
537 }
538
539 #[test]
540 fn split_string_cjk2() {
541 assert_eq!(
542 split_string_by_width("你好啊", 5, true),
543 vec![("你好", 1), ("啊", 3)]
544 );
545 }
546
547 #[test]
548 fn test_split_and_apply_missing() {
549 let res = split_and_apply("foo", 3, true, &[], Side::Left);
550 assert_eq!(res, vec![highlight_missing_style_bug("foo")])
551 }
552
553 #[test]
554 fn test_split_and_apply() {
555 let res = split_and_apply(
556 "foo",
557 3,
558 true,
559 &[(
560 SingleLineSpan {
561 line: 0.into(),
562 start_col: 0,
563 end_col: 3,
564 },
565 Style::new(),
566 )],
567 Side::Left,
568 );
569 assert_eq!(res, vec!["foo"])
570 }
571
572 #[test]
573 fn test_split_and_apply_trailing_text() {
574 let res = split_and_apply(
575 "foobar",
576 6,
577 true,
578 &[(
579 SingleLineSpan {
580 line: 0.into(),
581 start_col: 0,
582 end_col: 3,
583 },
584 Style::new(),
585 )],
586 Side::Left,
587 );
588 assert_eq!(res, vec!["foobar"])
589 }
590
591 #[test]
592 fn test_split_and_apply_gap_between_styles_on_wrap_boundary() {
593 let res = split_and_apply(
594 "foobar",
595 3,
596 true,
597 &[
598 (
599 SingleLineSpan {
600 line: 0.into(),
601 start_col: 0,
602 end_col: 2,
603 },
604 Style::new(),
605 ),
606 (
607 SingleLineSpan {
608 line: 0.into(),
609 start_col: 4,
610 end_col: 6,
611 },
612 Style::new(),
613 ),
614 ],
615 Side::Left,
616 );
617 assert_eq!(res, vec!["foo", "bar"])
618 }
619
620 #[test]
621 fn test_split_and_apply_trailing_text_newline() {
622 let res = split_and_apply(
623 "foobar ",
624 6,
625 true,
626 &[(
627 SingleLineSpan {
628 line: 0.into(),
629 start_col: 0,
630 end_col: 3,
631 },
632 Style::new(),
633 )],
634 Side::Left,
635 );
636 assert_eq!(res, vec!["foobar", " "])
637 }
638}