1mod formatter;
4mod render;
5mod strategy;
6mod types;
7pub(crate) mod wrap_sync;
8
9pub(crate) use formatter::*;
10pub(crate) use render::*;
11pub(crate) use strategy::*;
12pub use types::*;
13pub(crate) use wrap_sync::*;
14
15use std::cell::RefCell;
16use std::collections::HashMap;
17use std::hash::{Hash, Hasher};
18
19use crate::callback::Callback;
20use crate::core::element::Element;
21use crate::style::{Color, DiffPalette, Length, Style};
22use crate::widgets::{Divider, DocumentView, Frame, HStack, ScrollEvent, TextArea};
23use rustc_hash::FxHasher;
24use std::rc::Rc;
25use std::sync::Arc;
26
27const PANE_DATA_CACHE_LIMIT: usize = 2048;
30const DIFF_GUTTER_SPANS_CACHE_LIMIT: usize = 4096;
31const DIFF_DATA_CACHE_LIMIT: usize = 2048;
37
38thread_local! {
42 static DIFF_DATA_CACHE: RefCell<HashMap<u64, Arc<DiffData>>> =
43 RefCell::new(HashMap::new());
44 static PATCH_DIFF_DATA_PTR_CACHE: RefCell<HashMap<PatchDiffDataPtrKey, Arc<DiffData>>> =
45 RefCell::new(HashMap::new());
46 static PANE_DATA_CACHE: RefCell<HashMap<(PaneCacheKey, u64), PaneCacheEntry>> =
47 RefCell::new(HashMap::new());
48}
49
50fn diff_data_cache_key(before: &str, after: &str, config: &DiffDataConfig) -> u64 {
51 let mut h = FxHasher::default();
52 0u8.hash(&mut h);
53 before.hash(&mut h);
54 after.hash(&mut h);
55 config.hash(&mut h);
56 h.finish()
57}
58
59fn patch_diff_data_cache_key(patch: &str, config: &DiffDataConfig) -> u64 {
60 let mut h = FxHasher::default();
61 1u8.hash(&mut h);
62 patch.hash(&mut h);
63 config.hash(&mut h);
64 h.finish()
65}
66
67#[derive(Clone)]
68struct PatchDiffDataPtrKey {
69 patch: Arc<str>,
70 config: DiffDataConfig,
71}
72
73impl PartialEq for PatchDiffDataPtrKey {
74 fn eq(&self, other: &Self) -> bool {
75 Arc::ptr_eq(&self.patch, &other.patch) && self.config == other.config
76 }
77}
78
79impl Eq for PatchDiffDataPtrKey {}
80
81impl Hash for PatchDiffDataPtrKey {
82 fn hash<H: Hasher>(&self, state: &mut H) {
83 self.patch.as_ptr().hash(state);
84 self.patch.len().hash(state);
85 self.config.hash(state);
86 }
87}
88
89fn cached_diff_data(before: &str, after: &str, config: DiffDataConfig) -> Arc<DiffData> {
90 let key = diff_data_cache_key(before, after, &config);
91 DIFF_DATA_CACHE.with(|cache| {
92 let mut map = cache.borrow_mut();
93 if let Some(data) = map.get(&key) {
94 return Arc::clone(data);
95 }
96 let data = Arc::new(DiffData::with_config(before, after, config));
97 if map.len() >= DIFF_DATA_CACHE_LIMIT {
98 map.clear();
99 }
100 map.insert(key, Arc::clone(&data));
101 data
102 })
103}
104
105fn cached_patch_diff_data(patch: Arc<str>, config: DiffDataConfig) -> Arc<DiffData> {
106 let ptr_key = PatchDiffDataPtrKey {
107 patch: Arc::clone(&patch),
108 config: config.clone(),
109 };
110 if let Some(data) =
111 PATCH_DIFF_DATA_PTR_CACHE.with(|cache| cache.borrow().get(&ptr_key).cloned())
112 {
113 return data;
114 }
115
116 let content_key = patch_diff_data_cache_key(patch.as_ref(), &config);
117 let data = DIFF_DATA_CACHE.with(|cache| {
118 let mut map = cache.borrow_mut();
119 if let Some(data) = map.get(&content_key) {
120 return Arc::clone(data);
121 }
122 let data = Arc::new(DiffData::from_patch_with_config(patch.as_ref(), config));
123 if map.len() >= DIFF_DATA_CACHE_LIMIT {
124 map.clear();
125 }
126 map.insert(content_key, Arc::clone(&data));
127 data
128 });
129 PATCH_DIFF_DATA_PTR_CACHE.with(|cache| {
130 let mut cache = cache.borrow_mut();
131 if cache.len() >= DIFF_DATA_CACHE_LIMIT && !cache.contains_key(&ptr_key) {
132 cache.clear();
133 }
134 cache.insert(ptr_key, Arc::clone(&data));
135 });
136 data
137}
138
139#[cfg(feature = "syntax-syntect")]
140use crate::widgets::SyntectStrategy;
141use crate::widgets::TextAreaColorStrategy;
142
143#[derive(Clone, PartialEq, Eq, Hash)]
145pub(crate) struct PaneCacheKey {
146 diff_hash: u64,
147 mode: DiffViewMode,
148 pane: DiffPane,
149 line_numbers: bool,
150 min_digits: usize,
151}
152
153#[derive(Clone)]
155pub(crate) struct PaneCacheEntry {
156 key: PaneCacheKey,
157 gutter_style_hash: u64,
158 numbered_render: DiffRender,
159 gutter_spans: DiffGutterSpans,
160 gutter_col_width: u16,
161 excluded_source_lines: Arc<Vec<usize>>,
162 excluded_bytes: Arc<Vec<(usize, usize)>>,
163}
164
165#[derive(Clone, Copy)]
166pub(crate) struct PaneRenderOptions {
167 mode: DiffViewMode,
168 line_numbers: bool,
169 min_digits: usize,
170 style: DiffPalette,
171 gutter_style_hash: u64,
172}
173
174#[derive(Clone)]
176pub struct DiffView {
177 before: Arc<str>,
178 after: Arc<str>,
179 patch: Option<Arc<str>>,
180 mode: DiffViewMode,
181 backend: DiffViewBackend,
182 backend_explicit: bool,
183 width_override: Option<Length>,
184 height_override: Option<Length>,
185 editable: bool,
186 wrap_override: Option<bool>,
187 line_numbers_override: Option<bool>,
188 min_line_number_width_override: Option<u8>,
189 gutter_inset_override: Option<u16>,
190 scrollbar_override: Option<bool>,
191 h_scrollbar_override: Option<bool>,
192 focusable_override: Option<bool>,
193 outer_border: bool,
194 pane_border: bool,
195 highlight_full_width: bool,
196 single_scrollbar: bool,
197 join_frame: bool,
198 vertical_separator: bool,
199 vertical_separator_char: char,
200 vertical_separator_style: Style,
201 scroll_offset: Option<usize>,
202 scroll_to_hunk: Option<usize>,
203 on_scroll: Option<Callback<DiffScrollEvent>>,
204 on_context_separator_click: Option<Callback<DiffContextSeparatorEvent>>,
205 context_separator_hover_style: Option<Style>,
206 text_area: TextArea,
207 document_view: DocumentView,
208 diff_style: DiffPalette,
209 prefixes: DiffPrefixes,
210 show_prefixes: bool,
211 word_diff: bool,
212 trim_common_indent: bool,
213 shared_selection_id: Option<Arc<str>>,
214 language: Option<Arc<str>>,
215 theme: Option<Arc<str>>,
216 base_color_strategy: Option<Rc<dyn TextAreaColorStrategy>>,
217 diff_data: Option<Arc<DiffData>>,
218 context_lines: Option<usize>,
219 show_context_separator: bool,
220 context_separator_text: Arc<str>,
221 context_separator_min_lines: usize,
222 context_expand_lines: usize,
223 expanded_contexts: Vec<DiffContextExpansion>,
224 pane_cache: RefCell<Vec<PaneCacheEntry>>,
227}
228
229impl DiffView {
230 pub fn new() -> Self {
232 Self::new_internal("".into(), "".into(), None)
233 }
234
235 pub fn before(mut self, before: impl Into<Arc<str>>) -> Self {
237 self.before = before.into();
238 self
239 }
240
241 pub fn after(mut self, after: impl Into<Arc<str>>) -> Self {
243 self.after = after.into();
244 self
245 }
246
247 pub fn patch(mut self, patch: impl Into<Arc<str>>) -> Self {
249 let patch = patch.into();
250 self.text_area = self.text_area.value("");
251 self.document_view = self.document_view.value("");
252 self.before = "".into();
253 self.after = "".into();
254 self.diff_data = None;
255 self.patch = Some(patch);
256 self
257 }
258
259 pub fn with_content(before: impl Into<Arc<str>>, after: impl Into<Arc<str>>) -> Self {
261 Self::new_internal(before.into(), after.into(), None)
262 }
263
264 pub fn from_patch(patch: impl Into<Arc<str>>) -> Self {
270 Self::new().patch(patch)
271 }
272
273 fn new_internal(before: Arc<str>, after: Arc<str>, diff_data: Option<DiffData>) -> Self {
274 let text_area = TextArea::new("")
275 .read_only(true)
276 .line_numbers(true)
277 .wrap(false)
278 .scrollbar(true)
279 .h_scrollbar(true);
280 let document_view = DocumentView::new("")
281 .line_numbers(true)
282 .wrap(false)
283 .scrollbar(true)
284 .h_scrollbar(true);
285 Self {
286 before,
287 after,
288 patch: None,
289 mode: DiffViewMode::Split,
290 backend: DiffViewBackend::TextArea,
291 backend_explicit: false,
292 width_override: None,
293 height_override: None,
294 editable: false,
295 wrap_override: None,
296 line_numbers_override: None,
297 min_line_number_width_override: None,
298 gutter_inset_override: None,
299 scrollbar_override: None,
300 h_scrollbar_override: None,
301 focusable_override: None,
302 outer_border: false,
303 pane_border: true,
304 highlight_full_width: false,
305 single_scrollbar: false,
306 join_frame: false,
307 vertical_separator: false,
308 vertical_separator_char: '│',
309 vertical_separator_style: Style::default(),
310 scroll_offset: None,
311 scroll_to_hunk: None,
312 on_scroll: None,
313 on_context_separator_click: None,
314 context_separator_hover_style: None,
315 text_area,
316 document_view,
317 diff_style: DiffPalette::default(),
318 prefixes: DiffPrefixes::default(),
319 show_prefixes: true,
320 word_diff: true,
321 trim_common_indent: true,
322 shared_selection_id: None,
323 language: None,
324 theme: None,
325 base_color_strategy: None,
326 diff_data: diff_data.map(Arc::new),
327 context_lines: None,
328 show_context_separator: true,
329 context_separator_text: default_context_separator_text(),
330 context_separator_min_lines: default_context_separator_min_lines(),
331 context_expand_lines: default_context_expand_lines(),
332 expanded_contexts: Vec::new(),
333 pane_cache: RefCell::new(Vec::new()),
334 }
335 }
336
337 pub fn backend(mut self, backend: DiffViewBackend) -> Self {
342 self.backend = backend;
343 self.backend_explicit = true;
344 self
345 }
346
347 pub fn editable(mut self, editable: bool) -> Self {
351 self.editable = editable;
352 self
353 }
354
355 pub fn scroll_offset(mut self, offset: usize) -> Self {
357 self.scroll_offset = Some(offset);
358 self
359 }
360
361 pub fn scroll_to_hunk(mut self, index: usize) -> Self {
368 self.scroll_to_hunk = Some(index);
369 self
370 }
371
372 pub fn on_scroll(mut self, cb: Callback<DiffScrollEvent>) -> Self {
374 self.on_scroll = Some(cb);
375 self
376 }
377
378 pub fn on_context_separator_click(mut self, cb: Callback<DiffContextSeparatorEvent>) -> Self {
383 self.on_context_separator_click = Some(cb);
384 self
385 }
386
387 pub fn context_separator_hover_style(mut self, style: Style) -> Self {
389 self.context_separator_hover_style = Some(style);
390 self
391 }
392
393 pub fn mode(mut self, mode: DiffViewMode) -> Self {
395 self.mode = mode;
396 self
397 }
398
399 pub fn width(mut self, width: impl Into<Length>) -> Self {
401 self.width_override = Some(width.into());
402 self
403 }
404
405 pub fn height(mut self, height: impl Into<Length>) -> Self {
407 self.height_override = Some(height.into());
408 self
409 }
410
411 pub fn text_area(mut self, text_area: TextArea) -> Self {
416 if self.base_color_strategy.is_none() {
417 self.base_color_strategy = text_area.color_strategy.clone();
418 }
419 if self.language.is_none() {
420 self.language = text_area.language.clone();
421 }
422 if self.theme.is_none() {
423 self.theme = text_area.theme.clone();
424 }
425 self.text_area = text_area;
426 if let Some(v) = self.wrap_override {
427 self.text_area = self.text_area.wrap(v);
428 }
429 if let Some(v) = self.gutter_inset_override {
430 self.text_area = self.text_area.gutter_inset(v);
431 }
432 if let Some(v) = self.scrollbar_override {
433 self.text_area = self.text_area.scrollbar(v);
434 }
435 if let Some(v) = self.h_scrollbar_override {
436 self.text_area = self.text_area.h_scrollbar(v);
437 }
438 if let Some(v) = self.focusable_override {
439 self.text_area = self.text_area.focusable(v);
440 }
441 if !self.backend_explicit {
442 self.backend = DiffViewBackend::TextArea;
443 }
444 self
445 }
446
447 pub fn document_view(mut self, document_view: DocumentView) -> Self {
452 self.document_view = document_view;
453 if let Some(v) = self.wrap_override {
454 self.document_view = self.document_view.wrap(v);
455 }
456 if let Some(v) = self.gutter_inset_override {
457 self.document_view = self.document_view.gutter_inset(v);
458 }
459 if let Some(v) = self.scrollbar_override {
460 self.document_view = self.document_view.scrollbar(v);
461 }
462 if let Some(v) = self.h_scrollbar_override {
463 self.document_view = self.document_view.h_scrollbar(v);
464 }
465 if let Some(v) = self.focusable_override {
466 self.document_view = self.document_view.focusable(v);
467 }
468 if !self.backend_explicit {
469 self.backend = DiffViewBackend::DocumentView;
470 }
471 self
472 }
473
474 pub fn wrap(mut self, wrap: bool) -> Self {
476 self.wrap_override = Some(wrap);
477 self.text_area = self.text_area.wrap(wrap);
478 self.document_view = self.document_view.wrap(wrap);
479 self
480 }
481
482 pub fn line_numbers(mut self, show: bool) -> Self {
484 self.line_numbers_override = Some(show);
485 self
486 }
487
488 pub fn min_line_number_width(mut self, width: u8) -> Self {
490 self.min_line_number_width_override = Some(width);
491 self
492 }
493
494 pub fn border(mut self, border: bool) -> Self {
496 self.outer_border = border;
497 self
498 }
499
500 pub fn panels_border(mut self, border: bool) -> Self {
502 self.pane_border = border;
503 self
504 }
505
506 pub fn highlight_full_width(mut self, enabled: bool) -> Self {
508 self.highlight_full_width = enabled;
509 self
510 }
511
512 pub fn scrollbar(mut self, show: bool) -> Self {
514 self.scrollbar_override = Some(show);
515 self.text_area = self.text_area.scrollbar(show);
516 self.document_view = self.document_view.scrollbar(show);
517 self
518 }
519
520 pub fn gutter_inset(mut self, inset: u16) -> Self {
522 self.gutter_inset_override = Some(inset);
523 self.text_area = self.text_area.gutter_inset(inset);
524 self.document_view = self.document_view.gutter_inset(inset);
525 self
526 }
527
528 pub fn h_scrollbar(mut self, show: bool) -> Self {
530 self.h_scrollbar_override = Some(show);
531 self.text_area = self.text_area.h_scrollbar(show);
532 self.document_view = self.document_view.h_scrollbar(show);
533 self
534 }
535
536 pub fn focusable(mut self, focusable: bool) -> Self {
538 self.focusable_override = Some(focusable);
539 self.text_area = self.text_area.focusable(focusable);
540 self.document_view = self.document_view.focusable(focusable);
541 self
542 }
543
544 pub fn single_scrollbar(mut self, enabled: bool) -> Self {
546 self.single_scrollbar = enabled;
547 self
548 }
549
550 pub fn join_frame(mut self, join: bool) -> Self {
552 self.join_frame = join;
553 self
554 }
555
556 pub fn vertical_separator(mut self, enabled: bool) -> Self {
558 self.vertical_separator = enabled;
559 self
560 }
561
562 pub fn vertical_separator_char(mut self, ch: char) -> Self {
564 self.vertical_separator_char = ch;
565 self
566 }
567
568 pub fn vertical_separator_style(mut self, style: Style) -> Self {
570 self.vertical_separator_style = style;
571 self
572 }
573
574 pub fn diff_style(mut self, style: DiffPalette) -> Self {
576 self.diff_style = style;
577 self
578 }
579
580 pub fn neutral_bg(mut self, color: Color) -> Self {
584 self.diff_style.context = self.diff_style.context.bg(color);
585 self
586 }
587
588 pub fn shared_selection_id(mut self, id: impl Into<Arc<str>>) -> Self {
598 self.shared_selection_id = Some(id.into());
599 self
600 }
601
602 pub fn prefixes(mut self, prefixes: DiffPrefixes) -> Self {
604 self.prefixes = prefixes;
605 self
606 }
607
608 pub fn show_prefixes(mut self, show: bool) -> Self {
610 self.show_prefixes = show;
611 self
612 }
613
614 pub fn word_diff(mut self, enabled: bool) -> Self {
616 self.word_diff = enabled;
617 self
618 }
619
620 pub fn trim_common_indent(mut self, enabled: bool) -> Self {
627 self.trim_common_indent = enabled;
628 self
629 }
630
631 pub fn context_lines(mut self, n: usize) -> Self {
641 self.context_lines = Some(n);
642 self
643 }
644
645 pub fn show_context_separator(mut self, show: bool) -> Self {
652 self.show_context_separator = show;
653 self
654 }
655
656 pub fn context_separator_text(mut self, text: impl Into<Arc<str>>) -> Self {
664 self.context_separator_text = text.into();
665 self
666 }
667
668 pub fn context_separator_min_lines(mut self, min_lines: usize) -> Self {
673 self.context_separator_min_lines = min_lines.max(1);
674 self
675 }
676
677 pub fn context_expand_lines(mut self, lines: usize) -> Self {
682 self.context_expand_lines = lines.max(1);
683 self
684 }
685
686 pub fn expanded_contexts(mut self, ranges: impl IntoIterator<Item = DiffContextRange>) -> Self {
692 self.expanded_contexts = ranges.into_iter().map(DiffContextExpansion::full).collect();
693 self
694 }
695
696 pub fn expanded_context_expansions(
698 mut self,
699 expansions: impl IntoIterator<Item = DiffContextExpansion>,
700 ) -> Self {
701 self.expanded_contexts = expansions.into_iter().collect();
702 self
703 }
704
705 pub fn expanded_context(mut self, range: DiffContextRange) -> Self {
707 self.expanded_contexts
708 .push(DiffContextExpansion::full(range));
709 self
710 }
711
712 pub fn expanded_context_lines(
714 mut self,
715 range: DiffContextRange,
716 lines_revealed: usize,
717 ) -> Self {
718 self.expanded_contexts.push(DiffContextExpansion {
719 range,
720 lines_revealed,
721 });
722 self
723 }
724
725 pub fn base_color_strategy(mut self, strategy: impl TextAreaColorStrategy + 'static) -> Self {
731 self.base_color_strategy = Some(Rc::new(strategy));
732 self
733 }
734
735 pub fn language(mut self, language: impl Into<Arc<str>>) -> Self {
737 let language = language.into();
738 self.language = Some(language.clone());
739 self.text_area = self.text_area.language(language);
740 self
741 }
742
743 #[cfg(feature = "syntax-syntect")]
750 pub fn language_from_path(self, path: impl AsRef<std::path::Path>) -> Self {
751 if let Some(lang) = crate::widgets::language_from_path(path) {
752 self.language(lang)
753 } else {
754 self
755 }
756 }
757
758 pub fn theme(mut self, theme: impl Into<Arc<str>>) -> Self {
760 let theme = theme.into();
761 self.theme = Some(theme.clone());
762 self.text_area = self.text_area.theme(theme);
763 self
764 }
765
766 #[cfg(feature = "syntax-syntect")]
768 pub fn with_syntax(self, language: impl Into<Arc<str>>, theme: impl Into<Arc<str>>) -> Self {
769 self.base_color_strategy(SyntectStrategy::default())
770 .language(language)
771 .theme(theme)
772 }
773
774 #[cfg(feature = "syntax-syntect")]
776 pub fn with_syntax_bg(self, language: impl Into<Arc<str>>, theme: impl Into<Arc<str>>) -> Self {
777 self.base_color_strategy(SyntectStrategy::default().use_background(true))
778 .language(language)
779 .theme(theme)
780 }
781
782 pub fn with_diff(mut self, data: DiffData) -> Self {
784 self.patch = None;
785 self.diff_data = Some(Arc::new(data));
786 self
787 }
788
789 pub fn with_shared_diff(mut self, data: Arc<DiffData>) -> Self {
791 self.patch = None;
792 self.diff_data = Some(data);
793 self
794 }
795
796 fn resolved_width(&self) -> Length {
797 self.width_override.unwrap_or(match self.backend {
798 DiffViewBackend::TextArea => self.text_area.width,
799 DiffViewBackend::DocumentView => self.document_view.width,
800 })
801 }
802
803 fn effective_wrap(&self) -> bool {
804 self.wrap_override.unwrap_or(match self.backend {
805 DiffViewBackend::TextArea => self.text_area.wrap,
806 DiffViewBackend::DocumentView => self.document_view.wrap,
807 })
808 }
809
810 fn effective_scrollbar(&self) -> bool {
811 self.scrollbar_override.unwrap_or(match self.backend {
812 DiffViewBackend::TextArea => self.text_area.scrollbar,
813 DiffViewBackend::DocumentView => self.document_view.scrollbar,
814 })
815 }
816
817 fn backend_height(&self) -> Length {
818 match self.backend {
819 DiffViewBackend::TextArea => self.text_area.height,
820 DiffViewBackend::DocumentView => self.document_view.height,
821 }
822 }
823
824 fn should_use_implicit_auto_height(&self) -> bool {
825 self.height_override.is_none()
826 && self.effective_wrap()
827 && !self.effective_scrollbar()
828 && matches!(self.backend_height(), Length::Flex(1))
829 }
830
831 fn resolved_height(&self) -> Length {
832 self.height_override.unwrap_or_else(|| {
833 if self.should_use_implicit_auto_height() {
834 Length::Auto
835 } else {
836 self.backend_height()
837 }
838 })
839 }
840}
841
842impl Default for DiffView {
843 fn default() -> Self {
844 Self::new()
845 }
846}
847
848fn separator_click_config(
849 render: &DiffRender,
850 pane: DiffPane,
851 on_click: Option<Callback<DiffContextSeparatorEvent>>,
852 hover_style: Option<Style>,
853 expand_lines: usize,
854) -> Option<DiffContextSeparatorClickConfig> {
855 let has_interaction = on_click.is_some() || hover_style.is_some_and(|style| !style.is_empty());
856 if !has_interaction {
857 return None;
858 }
859 let events = render
860 .lines
861 .iter()
862 .map(|line| {
863 line.context_separator
864 .as_ref()
865 .map(|separator| separator.event(pane, expand_lines))
866 })
867 .collect::<Vec<_>>();
868 events
869 .iter()
870 .any(Option::is_some)
871 .then(|| DiffContextSeparatorClickConfig {
872 events_by_source_line: events.into(),
873 on_click,
874 hover_style,
875 })
876}
877
878impl From<DiffView> for Element {
879 fn from(view: DiffView) -> Self {
880 let outer_width = view.resolved_width();
881 let outer_height = view.resolved_height();
882 let use_auto_pane_height = matches!(outer_height, Length::Auto);
883 let config = DiffDataConfig {
884 prefixes: view.prefixes.clone(),
885 show_prefixes: view.show_prefixes,
886 word_diff: view.word_diff,
887 context_lines: None,
888 ..DiffDataConfig::default()
889 };
890 let diff_data = if let Some(data) = view.diff_data.clone() {
891 data
892 } else if let Some(patch) = view.patch.as_ref() {
893 cached_patch_diff_data(Arc::clone(patch), config)
894 } else {
895 cached_diff_data(&view.before, &view.after, config)
896 };
897 let (left_render, right_render, unified_render) = if view.trim_common_indent {
898 match view.mode {
899 DiffViewMode::Split => {
900 let trim = common_indent_across_lines(
901 diff_data
902 .left
903 .lines
904 .iter()
905 .chain(diff_data.right.lines.iter()),
906 );
907 (
908 trim_render_common_indent(&diff_data.left, trim),
909 trim_render_common_indent(&diff_data.right, trim),
910 trim_render_common_indent(
911 &diff_data.unified,
912 common_indent_across_lines(diff_data.unified.lines.iter()),
913 ),
914 )
915 }
916 DiffViewMode::Unified => {
917 let trim = common_indent_across_lines(diff_data.unified.lines.iter());
918 (
919 trim_render_common_indent(&diff_data.left, trim),
920 trim_render_common_indent(&diff_data.right, trim),
921 trim_render_common_indent(&diff_data.unified, trim),
922 )
923 }
924 }
925 } else {
926 (
927 diff_data.left.clone(),
928 diff_data.right.clone(),
929 diff_data.unified.clone(),
930 )
931 };
932
933 let (left_render, right_render, unified_render) =
934 apply_runtime_context_collapse_to_diff_renders(
935 left_render,
936 right_render,
937 unified_render,
938 view.context_lines,
939 render::context_collapse_options(
940 view.show_context_separator,
941 view.context_separator_text.as_ref(),
942 view.context_separator_min_lines,
943 &view.expanded_contexts,
944 ),
945 );
946
947 let base_strategy = view
948 .base_color_strategy
949 .clone()
950 .or_else(|| view.text_area.color_strategy.clone());
951
952 let line_numbers_enabled = view.line_numbers_override.unwrap_or(match view.backend {
953 DiffViewBackend::TextArea => view.text_area.line_numbers,
954 DiffViewBackend::DocumentView => view.document_view.line_numbers,
955 });
956 let min_line_digits = view
957 .min_line_number_width_override
958 .map(usize::from)
959 .unwrap_or(match view.backend {
960 DiffViewBackend::TextArea => view.text_area.min_line_number_width as usize,
961 DiffViewBackend::DocumentView => view.document_view.min_line_number_width as usize,
962 });
963
964 let language = view.language.clone();
965 let theme = view.theme.clone();
966 let split_wrap_sync = matches!(view.mode, DiffViewMode::Split) && view.effective_wrap();
967 let split_wrap_state = split_wrap_sync.then(new_split_wrap_sync_state);
968 if let Some(ref sync) = split_wrap_state {
969 wrap_sync::set_split_wrap_scrollbar_cols(
970 sync,
971 split_pane_standalone_scrollbar_cols(&view, DiffPane::Left),
972 split_pane_standalone_scrollbar_cols(&view, DiffPane::Right),
973 );
974 }
975 let split_wrap_padding_gutter_style = split_wrap_sync.then_some(
976 view.diff_style
977 .empty
978 .patch(view.diff_style.context_line_number),
979 );
980 let split_wrap_padding_style = split_wrap_sync.then_some(view.diff_style.empty);
981 let left_peer_source_lines = if split_wrap_sync {
982 Some(Arc::new(
983 right_render
984 .lines
985 .iter()
986 .map(|line| Arc::clone(&line.text))
987 .collect::<Vec<_>>(),
988 ))
989 } else {
990 None
991 };
992 let right_peer_source_lines = if split_wrap_sync {
993 Some(Arc::new(
994 left_render
995 .lines
996 .iter()
997 .map(|line| Arc::clone(&line.text))
998 .collect::<Vec<_>>(),
999 ))
1000 } else {
1001 None
1002 };
1003
1004 let pane_shared_selection_id = |pane: DiffPane| -> Option<Arc<str>> {
1008 let base = view.shared_selection_id.as_ref()?;
1009 Some(match (view.mode, pane) {
1010 (DiffViewMode::Unified, _) | (_, DiffPane::Unified) => Arc::clone(base),
1011 (DiffViewMode::Split, DiffPane::Left) => Arc::from(format!("{}:left", base)),
1012 (DiffViewMode::Split, DiffPane::Right) => Arc::from(format!("{}:right", base)),
1013 })
1014 };
1015
1016 let gutter_style_hash = diff_style_hash(&view.diff_style);
1017 let pane_options = PaneRenderOptions {
1018 mode: view.mode,
1019 line_numbers: line_numbers_enabled,
1020 min_digits: min_line_digits,
1021 style: view.diff_style,
1022 gutter_style_hash,
1023 };
1024
1025 let build_text_area = |render: &DiffRender, pane: DiffPane| {
1026 let pane_data = get_pane_data(&view.pane_cache, render, pane, pane_options);
1027 let render = pane_data.numbered_render;
1028 let gutter_spans = pane_data.gutter_spans;
1029 let gutter_col_width = pane_data.gutter_col_width;
1030 let excluded_source_lines = pane_data.excluded_source_lines;
1031 let excluded_bytes = pane_data.excluded_bytes;
1032 let diff_strategy = DiffColorStrategy::new(
1033 render.clone(),
1034 base_strategy.clone(),
1035 view.diff_style,
1036 view.highlight_full_width,
1037 false,
1038 );
1039 let separator_click = separator_click_config(
1040 &render,
1041 pane,
1042 view.on_context_separator_click.clone(),
1043 view.context_separator_hover_style,
1044 view.context_expand_lines,
1045 );
1046 let scroll_to_hunk_line = view
1047 .scroll_to_hunk
1048 .and_then(|hunk_index| hunk_logical_line(&render, hunk_index));
1049 let mut area = view
1050 .text_area
1051 .clone()
1052 .value(render.raw_text.clone())
1053 .read_only(!view.editable)
1054 .color_strategy(diff_strategy)
1055 .gutter_lines(gutter_spans, gutter_col_width)
1056 .copy_excluded_bytes(excluded_bytes)
1057 .selection_excluded_lines(excluded_source_lines);
1058
1059 area = area.line_numbers(false);
1060
1061 area.peer_source_lines = match pane {
1062 DiffPane::Left => left_peer_source_lines.clone(),
1063 DiffPane::Right => right_peer_source_lines.clone(),
1064 DiffPane::Unified => None,
1065 };
1066 area.split_wrap_sync = split_wrap_state.clone();
1067 area.split_wrap_side = match pane {
1068 DiffPane::Left => Some(SplitPaneSide::Left),
1069 DiffPane::Right => Some(SplitPaneSide::Right),
1070 DiffPane::Unified => None,
1071 };
1072 area.split_wrap_padding_gutter_style = split_wrap_padding_gutter_style;
1073 area.split_wrap_padding_style = split_wrap_padding_style;
1074 area.diff_context_separator_click = separator_click;
1075
1076 if let Some(v) = view.wrap_override {
1077 area = area.wrap(v);
1078 }
1079 if let Some(v) = view.scrollbar_override {
1080 area = area.scrollbar(v);
1081 }
1082 if let Some(v) = view.h_scrollbar_override {
1083 area = area.h_scrollbar(v);
1084 }
1085 if let Some(v) = view.focusable_override {
1086 area = area.focusable(v);
1087 }
1088
1089 if view.single_scrollbar && matches!(view.mode, DiffViewMode::Split) {
1090 let is_right = matches!(pane, DiffPane::Right);
1091 area = area.scrollbar(is_right);
1092 area.pin_scrollbar_focus_style = is_right;
1093 }
1094
1095 area = area.border(false);
1097
1098 if use_auto_pane_height {
1099 area = area.height(Length::Auto);
1100 }
1101
1102 if let Some(offset) = view.scroll_offset {
1103 area = area.scroll_offset(offset);
1104 }
1105 if let Some(line) = scroll_to_hunk_line {
1106 area = area.scroll_to_line(line);
1107 }
1108
1109 let existing_on_scroll = area.on_scroll.clone();
1110 let view_on_scroll = view.on_scroll.clone();
1111 if existing_on_scroll.is_some() || view_on_scroll.is_some() {
1112 area = area.on_scroll(Callback::new(move |event: ScrollEvent| {
1113 if let Some(cb) = &existing_on_scroll {
1114 cb.emit(event);
1115 }
1116 if let Some(cb) = &view_on_scroll {
1117 cb.emit(DiffScrollEvent {
1118 pane,
1119 scroll: event,
1120 });
1121 }
1122 }));
1123 }
1124
1125 area.into()
1126 };
1127
1128 let build_document_view = |render: &DiffRender, pane: DiffPane| {
1129 let pane_data = get_pane_data(&view.pane_cache, render, pane, pane_options);
1130 let render = pane_data.numbered_render;
1131 let gutter_spans = pane_data.gutter_spans;
1132 let gutter_col_width = pane_data.gutter_col_width;
1133 let excluded_source_lines = pane_data.excluded_source_lines;
1134 let formatter = DiffDocumentFormatter::new(
1135 render.clone(),
1136 base_strategy.clone(),
1137 view.diff_style,
1138 view.highlight_full_width,
1139 false,
1140 language.clone(),
1141 theme.clone(),
1142 );
1143 let separator_click = separator_click_config(
1144 &render,
1145 pane,
1146 view.on_context_separator_click.clone(),
1147 view.context_separator_hover_style,
1148 view.context_expand_lines,
1149 );
1150 let scroll_to_hunk_line = view
1151 .scroll_to_hunk
1152 .and_then(|hunk_index| hunk_logical_line(&render, hunk_index));
1153 let mut doc = view.document_view.clone();
1154 doc.value = render.raw_text.clone();
1155 doc.content_type = Some("diff".into());
1156 doc.formatter = Some(Rc::new(formatter));
1157 doc.highlight_full_width = view.highlight_full_width;
1158 doc.gutter_lines = Some(gutter_spans);
1159 doc.gutter_col_width = gutter_col_width;
1160 doc.copy_excluded_source_lines = Some(excluded_source_lines);
1161 doc.shared_selection_id = pane_shared_selection_id(pane);
1162 doc.peer_source_lines = match pane {
1163 DiffPane::Left => left_peer_source_lines.clone(),
1164 DiffPane::Right => right_peer_source_lines.clone(),
1165 DiffPane::Unified => None,
1166 };
1167 doc.split_wrap_sync = split_wrap_state.clone();
1168 doc.split_wrap_side = match pane {
1169 DiffPane::Left => Some(SplitPaneSide::Left),
1170 DiffPane::Right => Some(SplitPaneSide::Right),
1171 DiffPane::Unified => None,
1172 };
1173 doc.diff_split_pane = if matches!(view.mode, DiffViewMode::Split) {
1174 Some(pane)
1175 } else {
1176 None
1177 };
1178 doc.split_wrap_padding_gutter_style = split_wrap_padding_gutter_style;
1179 doc.split_wrap_padding_style = split_wrap_padding_style;
1180 doc.diff_context_separator_click = separator_click;
1181
1182 doc = doc.line_numbers(false);
1183
1184 if let Some(v) = view.wrap_override {
1185 doc = doc.wrap(v);
1186 }
1187 if let Some(v) = view.scrollbar_override {
1188 doc = doc.scrollbar(v);
1189 }
1190 if let Some(v) = view.h_scrollbar_override {
1191 doc = doc.h_scrollbar(v);
1192 }
1193 if let Some(v) = view.focusable_override {
1194 doc = doc.focusable(v);
1195 }
1196
1197 if view.single_scrollbar && matches!(view.mode, DiffViewMode::Split) {
1198 let is_right = matches!(pane, DiffPane::Right);
1199 doc = doc.scrollbar(is_right);
1200 doc.pin_scrollbar_focus_style = is_right;
1201 }
1202
1203 doc = doc.border(false);
1205
1206 if use_auto_pane_height {
1207 doc.height = Length::Auto;
1208 }
1209
1210 if let Some(offset) = view.scroll_offset {
1211 doc.scroll_offset = Some(offset);
1212 }
1213 if let Some(line) = scroll_to_hunk_line {
1214 doc = doc.scroll_to_source_line(line);
1215 }
1216
1217 let existing_on_scroll = doc.on_scroll.clone();
1218 let view_on_scroll = view.on_scroll.clone();
1219 if existing_on_scroll.is_some() || view_on_scroll.is_some() {
1220 doc.on_scroll = Some(Callback::new(move |event: ScrollEvent| {
1221 if let Some(cb) = &existing_on_scroll {
1222 cb.emit(event);
1223 }
1224 if let Some(cb) = &view_on_scroll {
1225 cb.emit(DiffScrollEvent {
1226 pane,
1227 scroll: event,
1228 });
1229 }
1230 }));
1231 }
1232
1233 doc.into()
1234 };
1235
1236 let build_pane = |render: &DiffRender, pane: DiffPane, is_left: bool| -> Element {
1237 let inner: Element = match view.backend {
1238 DiffViewBackend::TextArea => build_text_area(render, pane),
1239 DiffViewBackend::DocumentView => build_document_view(render, pane),
1240 };
1241
1242 let right_pad = if is_left && view.single_scrollbar && view.effective_scrollbar() {
1243 1
1244 } else {
1245 0
1246 };
1247
1248 let mut pane = Frame::new()
1249 .border(view.pane_border)
1250 .join_frame(view.join_frame)
1251 .padding((0, right_pad, 0, 0))
1252 .child(inner);
1253
1254 if use_auto_pane_height {
1255 pane = pane.height(Length::Auto);
1256 }
1257
1258 pane.into()
1259 };
1260
1261 let content = match view.mode {
1262 DiffViewMode::Split => {
1263 let left = build_pane(&left_render, DiffPane::Left, true);
1264 let right = build_pane(&right_render, DiffPane::Right, false);
1265 if view.vertical_separator {
1266 HStack::new()
1267 .even_flex(true)
1268 .child(left)
1269 .child(
1270 Divider::vertical()
1271 .ch(view.vertical_separator_char)
1272 .style(view.vertical_separator_style)
1273 .join_frame(view.join_frame),
1274 )
1275 .child(right)
1276 .into()
1277 } else {
1278 HStack::new()
1279 .even_flex(true)
1280 .child(left)
1281 .child(right)
1282 .into()
1283 }
1284 }
1285 DiffViewMode::Unified => build_pane(&unified_render, DiffPane::Unified, false),
1286 };
1287
1288 Frame::new()
1289 .border(view.outer_border)
1290 .width(outer_width)
1291 .height(outer_height)
1292 .padding(0)
1293 .child(content)
1294 .into()
1295 }
1296}
1297
1298mod gutter;
1299pub(crate) use gutter::*;
1300
1301#[cfg(test)]
1302mod tests;