@@ -27,6 +27,23 @@ use crate::overlays::{
2727static SYNTAX_SET : Lazy < SyntaxSet > = Lazy :: new ( SyntaxSet :: load_defaults_newlines) ;
2828static THEME_SET : Lazy < ThemeSet > = Lazy :: new ( ThemeSet :: load_defaults) ;
2929
30+ // ---------------------------------------------------------------------------
31+ // Diff palette — tuned to match Claude Code's terminal diff:
32+ // • dim red/green tint across the entire row for removed/added lines
33+ // • brighter highlight bg on inline word-level changes inside that row
34+ // • soft red/green foreground for markers so they pop on the tint
35+ // • subtle slate bg for hunk headers
36+ // ---------------------------------------------------------------------------
37+ const DIFF_BG_REMOVED : Color = Color :: Rgb ( 52 , 18 , 24 ) ; // dim red row tint
38+ const DIFF_BG_ADDED : Color = Color :: Rgb ( 14 , 44 , 22 ) ; // dim green row tint
39+ const DIFF_BG_WORD_DEL : Color = Color :: Rgb ( 150 , 38 , 52 ) ; // bright red — changed word
40+ const DIFF_BG_WORD_INS : Color = Color :: Rgb ( 34 , 120 , 52 ) ; // bright green — changed word
41+ const DIFF_FG_REMOVED : Color = Color :: Rgb ( 255 , 168 , 178 ) ; // soft red text/marker
42+ const DIFF_FG_ADDED : Color = Color :: Rgb ( 168 , 240 , 184 ) ; // soft green text/marker
43+ const DIFF_FG_GUTTER : Color = Color :: Rgb ( 108 , 108 , 122 ) ; // dim line-number gutter
44+ const DIFF_FG_HEADER : Color = Color :: Rgb ( 167 , 139 , 250 ) ; // hunk header (@@ lines)
45+ const DIFF_BG_HEADER : Color = Color :: Rgb ( 18 , 18 , 28 ) ; // subtle slate band
46+
3047// ---------------------------------------------------------------------------
3148// Data types
3249// ---------------------------------------------------------------------------
@@ -744,18 +761,20 @@ fn render_diff_detail(state: &DiffViewerState, area: Rect, buf: &mut Buffer) {
744761 return ;
745762 }
746763
747- // Build lines for rendering
748- let lines = build_diff_lines ( file, inner. width ) ;
749- let total_lines = lines. len ( ) ;
750- let scroll = ( state. detail_scroll as usize ) . min ( total_lines. saturating_sub ( inner. height as usize ) ) ;
751- let visible = & lines[ scroll..] ;
752-
753- // Shrink inner width by 1 to leave room for scrollbar
754- let text_width = if total_lines > inner. height as usize {
764+ // The diff lines are width-padded so the bg tint extends across the panel,
765+ // so pre-compute the rendered width and pass it in (avoids re-padding past
766+ // the scrollbar column).
767+ let raw_line_count: usize = file. hunks . iter ( ) . map ( |h| h. lines . len ( ) ) . sum ( ) ;
768+ let needs_scrollbar = raw_line_count > inner. height as usize ;
769+ let text_width = if needs_scrollbar {
755770 inner. width . saturating_sub ( 1 )
756771 } else {
757772 inner. width
758773 } ;
774+ let lines = build_diff_lines ( file, text_width) ;
775+ let total_lines = lines. len ( ) ;
776+ let scroll = ( state. detail_scroll as usize ) . min ( total_lines. saturating_sub ( inner. height as usize ) ) ;
777+ let visible = & lines[ scroll..] ;
759778
760779 for ( i, line) in visible. iter ( ) . enumerate ( ) {
761780 if i as u16 >= inner. height { break ; }
@@ -853,13 +872,19 @@ fn build_inline_diff_spans(old: &str, new: &str) -> (Vec<Span<'static>>, Vec<Spa
853872 ChangeTag :: Delete => {
854873 old_spans. push ( Span :: styled (
855874 s,
856- Style :: default ( ) . fg ( Color :: White ) . bg ( Color :: Rgb ( 150 , 30 , 30 ) ) ,
875+ Style :: default ( )
876+ . fg ( Color :: White )
877+ . bg ( DIFF_BG_WORD_DEL )
878+ . add_modifier ( Modifier :: BOLD ) ,
857879 ) ) ;
858880 }
859881 ChangeTag :: Insert => {
860882 new_spans. push ( Span :: styled (
861883 s,
862- Style :: default ( ) . fg ( Color :: White ) . bg ( Color :: Rgb ( 30 , 130 , 30 ) ) ,
884+ Style :: default ( )
885+ . fg ( Color :: White )
886+ . bg ( DIFF_BG_WORD_INS )
887+ . add_modifier ( Modifier :: BOLD ) ,
863888 ) ) ;
864889 }
865890 }
@@ -933,7 +958,8 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec<Line<'static>> {
933958 // Gutter = 10 chars ("dddd dddd "), prefix marker = 3 chars ("+ " etc.)
934959 let gutter_width: usize = 10 ;
935960 let prefix_width: usize = 3 ;
936- let avail = ( width as usize ) . saturating_sub ( gutter_width + prefix_width) ;
961+ let total_width = width as usize ;
962+ let avail = total_width. saturating_sub ( gutter_width + prefix_width) ;
937963
938964 for hunk in & file. hunks {
939965 let hunk_lines = & hunk. lines ;
@@ -951,21 +977,43 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec<Line<'static>> {
951977 let mut removed_row = vec ! [
952978 Span :: styled(
953979 format_gutter( diff_line. old_line_no, None ) ,
954- Style :: default ( ) . fg( Color :: DarkGray ) ,
980+ Style :: default ( ) . fg( DIFF_FG_GUTTER ) . bg ( DIFF_BG_REMOVED ) ,
955981 ) ,
956- Span :: styled( "- " , Style :: default ( ) . fg( Color :: Red ) ) ,
982+ Span :: styled(
983+ "- " ,
984+ Style :: default ( )
985+ . fg( DIFF_FG_REMOVED )
986+ . bg( DIFF_BG_REMOVED )
987+ . add_modifier( Modifier :: BOLD ) ,
988+ ) ,
989+ Span :: styled( " " , Style :: default ( ) . bg( DIFF_BG_REMOVED ) ) ,
957990 ] ;
958- removed_row. extend ( truncate_spans_to_width ( old_spans, avail) ) ;
991+ let mut old_clipped = truncate_spans_to_width ( old_spans, avail) ;
992+ recolor_equal_spans ( & mut old_clipped, DIFF_FG_REMOVED ) ;
993+ apply_row_bg ( & mut old_clipped, DIFF_BG_REMOVED ) ;
994+ removed_row. extend ( old_clipped) ;
995+ pad_to_width ( & mut removed_row, total_width, DIFF_BG_REMOVED ) ;
959996 lines. push ( Line :: from ( removed_row) ) ;
960997
961998 let mut added_row = vec ! [
962999 Span :: styled(
9631000 format_gutter( None , next_line. new_line_no) ,
964- Style :: default ( ) . fg( Color :: DarkGray ) ,
1001+ Style :: default ( ) . fg( DIFF_FG_GUTTER ) . bg ( DIFF_BG_ADDED ) ,
9651002 ) ,
966- Span :: styled( "+ " , Style :: default ( ) . fg( Color :: Green ) ) ,
1003+ Span :: styled(
1004+ "+ " ,
1005+ Style :: default ( )
1006+ . fg( DIFF_FG_ADDED )
1007+ . bg( DIFF_BG_ADDED )
1008+ . add_modifier( Modifier :: BOLD ) ,
1009+ ) ,
1010+ Span :: styled( " " , Style :: default ( ) . bg( DIFF_BG_ADDED ) ) ,
9671011 ] ;
968- added_row. extend ( truncate_spans_to_width ( new_spans, avail) ) ;
1012+ let mut new_clipped = truncate_spans_to_width ( new_spans, avail) ;
1013+ recolor_equal_spans ( & mut new_clipped, DIFF_FG_ADDED ) ;
1014+ apply_row_bg ( & mut new_clipped, DIFF_BG_ADDED ) ;
1015+ added_row. extend ( new_clipped) ;
1016+ pad_to_width ( & mut added_row, total_width, DIFF_BG_ADDED ) ;
9691017 lines. push ( Line :: from ( added_row) ) ;
9701018
9711019 i += 2 ;
@@ -974,42 +1022,64 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec<Line<'static>> {
9741022 }
9751023 }
9761024
977- // Standard single-line rendering
978- let ( marker, content_style) = match diff_line. kind {
979- DiffLineKind :: Header => (
980- Span :: styled ( "@@ " , Style :: default ( ) . fg ( Color :: Rgb ( 167 , 139 , 250 ) ) ) ,
981- Style :: default ( ) . fg ( Color :: Rgb ( 167 , 139 , 250 ) ) ,
982- ) ,
983- DiffLineKind :: Added => (
984- Span :: styled ( "+ " , Style :: default ( ) . fg ( Color :: Green ) ) ,
985- Style :: default ( ) . fg ( Color :: Green ) ,
986- ) ,
987- DiffLineKind :: Removed => (
988- Span :: styled ( "- " , Style :: default ( ) . fg ( Color :: Red ) ) ,
989- Style :: default ( ) . fg ( Color :: Red ) ,
990- ) ,
991- DiffLineKind :: Context => (
992- Span :: styled ( " " , Style :: default ( ) . fg ( Color :: DarkGray ) ) ,
993- Style :: default ( ) . fg ( Color :: White ) ,
994- ) ,
1025+ // Hunk header: render the @@ line full-width on a slate band, no gutter/marker.
1026+ if diff_line. kind == DiffLineKind :: Header {
1027+ let content: String = diff_line. content . chars ( ) . take ( total_width) . collect ( ) ;
1028+ let mut row = vec ! [ Span :: styled(
1029+ format!( " {} " , content) ,
1030+ Style :: default ( )
1031+ . fg( DIFF_FG_HEADER )
1032+ . bg( DIFF_BG_HEADER )
1033+ . add_modifier( Modifier :: BOLD ) ,
1034+ ) ] ;
1035+ pad_to_width ( & mut row, total_width, DIFF_BG_HEADER ) ;
1036+ lines. push ( Line :: from ( row) ) ;
1037+ i += 1 ;
1038+ continue ;
1039+ }
1040+
1041+ // Added / Removed / Context rows.
1042+ let ( marker_text, marker_fg, row_bg, fallback_fg) : ( & str , Color , Option < Color > , Color ) =
1043+ match diff_line. kind {
1044+ DiffLineKind :: Added => ( "+ " , DIFF_FG_ADDED , Some ( DIFF_BG_ADDED ) , DIFF_FG_ADDED ) ,
1045+ DiffLineKind :: Removed => ( "- " , DIFF_FG_REMOVED , Some ( DIFF_BG_REMOVED ) , DIFF_FG_REMOVED ) ,
1046+ DiffLineKind :: Context => ( " " , DIFF_FG_GUTTER , None , COVEN_CODE_TEXT ) ,
1047+ DiffLineKind :: Header => unreachable ! ( ) ,
1048+ } ;
1049+
1050+ let gutter_style = match row_bg {
1051+ Some ( bg) => Style :: default ( ) . fg ( DIFF_FG_GUTTER ) . bg ( bg) ,
1052+ None => Style :: default ( ) . fg ( DIFF_FG_GUTTER ) ,
1053+ } ;
1054+ let marker_style = match row_bg {
1055+ Some ( bg) => Style :: default ( ) . fg ( marker_fg) . bg ( bg) . add_modifier ( Modifier :: BOLD ) ,
1056+ None => Style :: default ( ) . fg ( marker_fg) ,
9951057 } ;
9961058
9971059 let ln_str = format_gutter ( diff_line. old_line_no , diff_line. new_line_no ) ;
9981060 let content: String = diff_line. content . chars ( ) . take ( avail) . collect ( ) ;
9991061
10001062 let mut row = vec ! [
1001- Span :: styled( ln_str, Style :: default ( ) . fg( Color :: DarkGray ) ) ,
1002- marker,
1063+ Span :: styled( ln_str, gutter_style) ,
1064+ Span :: styled( marker_text. to_string( ) , marker_style) ,
1065+ Span :: styled( " " , row_bg. map( |bg| Style :: default ( ) . bg( bg) ) . unwrap_or_default( ) ) ,
10031066 ] ;
10041067
1005- // Apply syntax highlighting for code lines (not headers)
1006- if diff_line. kind == DiffLineKind :: Header {
1007- row. push ( Span :: styled ( content, content_style) ) ;
1008- } else {
1009- let highlighted = highlight_code_line ( & content, & file. path , content_style) ;
1010- row. extend ( highlighted) ;
1068+ // Syntax-highlight the content. The fallback fg is used only for ranges
1069+ // where syntect returned a near-default color (so the diff tint reads cleanly).
1070+ let mut highlighted = highlight_code_line (
1071+ & content,
1072+ & file. path ,
1073+ Style :: default ( ) . fg ( fallback_fg) ,
1074+ ) ;
1075+ if let Some ( bg) = row_bg {
1076+ apply_row_bg ( & mut highlighted, bg) ;
10111077 }
1078+ row. extend ( highlighted) ;
10121079
1080+ if let Some ( bg) = row_bg {
1081+ pad_to_width ( & mut row, total_width, bg) ;
1082+ }
10131083 lines. push ( Line :: from ( row) ) ;
10141084
10151085 i += 1 ;
@@ -1019,6 +1089,39 @@ fn build_diff_lines(file: &FileDiffStats, width: u16) -> Vec<Line<'static>> {
10191089 lines
10201090}
10211091
1092+ /// Add `bg` to every span that doesn't already declare a background.
1093+ /// Preserves word-level inline highlight bgs (which are already set).
1094+ fn apply_row_bg ( spans : & mut Vec < Span < ' static > > , bg : Color ) {
1095+ for span in spans. iter_mut ( ) {
1096+ if span. style . bg . is_none ( ) {
1097+ span. style = span. style . bg ( bg) ;
1098+ }
1099+ }
1100+ }
1101+
1102+ /// Pad `spans` out to `width` characters by appending a single bg-only space span.
1103+ fn pad_to_width ( spans : & mut Vec < Span < ' static > > , width : usize , bg : Color ) {
1104+ let used: usize = spans. iter ( ) . map ( |s| s. content . chars ( ) . count ( ) ) . sum ( ) ;
1105+ if used < width {
1106+ spans. push ( Span :: styled (
1107+ " " . repeat ( width - used) ,
1108+ Style :: default ( ) . bg ( bg) ,
1109+ ) ) ;
1110+ }
1111+ }
1112+
1113+ /// Inside an inline-diff row, the COVEN_CODE_TEXT fg used by `build_inline_diff_spans`
1114+ /// for the unchanged-equal segments is too neutral against the dim red/green row tint.
1115+ /// Recolor those plain-text spans with `fg` so the row reads as a single coherent
1116+ /// removed/added block, while leaving the highlighted word spans alone.
1117+ fn recolor_equal_spans ( spans : & mut Vec < Span < ' static > > , fg : Color ) {
1118+ for span in spans. iter_mut ( ) {
1119+ if span. style . bg . is_none ( ) {
1120+ span. style = span. style . fg ( fg) ;
1121+ }
1122+ }
1123+ }
1124+
10221125// ---------------------------------------------------------------------------
10231126// Tests
10241127// ---------------------------------------------------------------------------
0 commit comments