• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

facet-rs / facet / 20108266382

10 Dec 2025 05:54PM UTC coverage: 57.724% (-0.8%) from 58.573%
20108266382

Pull #1220

github

web-flow
Merge 20d54c3e7 into fe1531898
Pull Request #1220: feat(cinereus): improve tree matching for leaf nodes and filter no-op moves

1769 of 3627 new or added lines in 18 files covered. (48.77%)

12 existing lines in 1 file now uncovered.

28569 of 49492 relevant lines covered (57.72%)

808.57 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

45.73
/facet-diff-core/src/theme.rs
1
//! Color themes for diff rendering.
2

3
use owo_colors::Rgb;
4
use palette::{FromColor, Lch, LinSrgb, Mix, Srgb};
5

6
/// Color theme for diff rendering.
7
///
8
/// Defines colors for different kinds of changes. The default uses
9
/// colorblind-friendly yellow/blue with type-specific value colors.
10
#[derive(Debug, Clone, PartialEq, Eq)]
11
pub struct DiffTheme {
12
    /// Foreground color for deleted content (accent color)
13
    pub deleted: Rgb,
14

15
    /// Foreground color for inserted content (accent color)
16
    pub inserted: Rgb,
17

18
    /// Foreground color for moved content (accent color)
19
    pub moved: Rgb,
20

21
    /// Foreground color for unchanged content
22
    pub unchanged: Rgb,
23

24
    /// Foreground color for keys/field names
25
    pub key: Rgb,
26

27
    /// Foreground color for structural elements like braces, brackets
28
    pub structure: Rgb,
29

30
    /// Foreground color for comments and type hints
31
    pub comment: Rgb,
32

33
    // === Value type base colors ===
34
    /// Base color for string values
35
    pub string: Rgb,
36

37
    /// Base color for numeric values (integers, floats)
38
    pub number: Rgb,
39

40
    /// Base color for boolean values
41
    pub boolean: Rgb,
42

43
    /// Base color for null/None values
44
    pub null: Rgb,
45

46
    /// Subtle background for deleted lines (None = no background)
47
    pub deleted_line_bg: Option<Rgb>,
48

49
    /// Stronger background highlight for changed values on deleted lines
50
    pub deleted_highlight_bg: Option<Rgb>,
51

52
    /// Subtle background for inserted lines (None = no background)
53
    pub inserted_line_bg: Option<Rgb>,
54

55
    /// Stronger background highlight for changed values on inserted lines
56
    pub inserted_highlight_bg: Option<Rgb>,
57

58
    /// Subtle background for moved lines (None = no background)
59
    pub moved_line_bg: Option<Rgb>,
60

61
    /// Stronger background highlight for changed values on moved lines
62
    pub moved_highlight_bg: Option<Rgb>,
63
}
64

65
impl Default for DiffTheme {
66
    fn default() -> Self {
2✔
67
        Self::COLORBLIND_WITH_BG
2✔
68
    }
2✔
69
}
70

71
impl DiffTheme {
72
    /// Colorblind-friendly theme - orange vs blue. No backgrounds.
73
    pub const COLORBLIND_ORANGE_BLUE: Self = Self {
74
        deleted: Rgb(255, 167, 89),    // #ffa759 warm orange
75
        inserted: Rgb(97, 175, 239),   // #61afef sky blue
76
        moved: Rgb(198, 120, 221),     // #c678dd purple/magenta
77
        unchanged: Rgb(140, 140, 140), // #8c8c8c medium gray (muted)
78
        key: Rgb(140, 140, 140),       // #8c8c8c medium gray
79
        structure: Rgb(220, 220, 220), // #dcdcdc light gray (structural elements)
80
        comment: Rgb(100, 100, 100),   // #646464 dark gray (very muted)
81
        string: Rgb(152, 195, 121),    // #98c379 green (like One Dark Pro)
82
        number: Rgb(209, 154, 102),    // #d19a66 orange
83
        boolean: Rgb(209, 154, 102),   // #d19a66 orange
84
        null: Rgb(86, 182, 194),       // #56b6c2 cyan
85
        deleted_line_bg: None,
86
        deleted_highlight_bg: None,
87
        inserted_line_bg: None,
88
        inserted_highlight_bg: None,
89
        moved_line_bg: None,
90
        moved_highlight_bg: None,
91
    };
92

93
    /// Colorblind-friendly with line + highlight backgrounds (yellow/blue).
94
    pub const COLORBLIND_WITH_BG: Self = Self {
95
        deleted: Rgb(229, 192, 123),   // #e5c07b warm yellow/gold
96
        inserted: Rgb(97, 175, 239),   // #61afef sky blue
97
        moved: Rgb(198, 120, 221),     // #c678dd purple/magenta
98
        unchanged: Rgb(140, 140, 140), // #8c8c8c medium gray (muted)
99
        key: Rgb(140, 140, 140),       // #8c8c8c medium gray
100
        structure: Rgb(220, 220, 220), // #dcdcdc light gray (structural elements)
101
        comment: Rgb(100, 100, 100),   // #646464 dark gray (very muted)
102
        string: Rgb(152, 195, 121),    // #98c379 green (like One Dark Pro)
103
        number: Rgb(209, 154, 102),    // #d19a66 orange
104
        boolean: Rgb(209, 154, 102),   // #d19a66 orange
105
        null: Rgb(86, 182, 194),       // #56b6c2 cyan
106
        // Subtle line backgrounds
107
        deleted_line_bg: Some(Rgb(55, 48, 35)), // medium-dark warm yellow
108
        inserted_line_bg: Some(Rgb(35, 48, 60)), // medium-dark cool blue
109
        moved_line_bg: Some(Rgb(50, 40, 60)),   // medium-dark purple
110
        // Stronger highlight backgrounds for changed values
111
        deleted_highlight_bg: Some(Rgb(90, 75, 50)), // medium yellow/brown
112
        inserted_highlight_bg: Some(Rgb(45, 70, 95)), // medium blue
113
        moved_highlight_bg: Some(Rgb(80, 55, 95)),   // medium purple
114
    };
115

116
    /// Pastel color theme - soft but distinguishable (not colorblind-friendly).
117
    pub const PASTEL: Self = Self {
118
        deleted: Rgb(255, 138, 128),   // #ff8a80 saturated coral/salmon
119
        inserted: Rgb(128, 203, 156),  // #80cb9c saturated mint green
120
        moved: Rgb(128, 179, 255),     // #80b3ff saturated sky blue
121
        unchanged: Rgb(140, 140, 140), // #8c8c8c medium gray (muted)
122
        key: Rgb(140, 140, 140),       // #8c8c8c medium gray
123
        structure: Rgb(220, 220, 220), // #dcdcdc light gray (structural elements)
124
        comment: Rgb(100, 100, 100),   // #646464 dark gray (very muted)
125
        string: Rgb(152, 195, 121),    // #98c379 green
126
        number: Rgb(209, 154, 102),    // #d19a66 orange
127
        boolean: Rgb(209, 154, 102),   // #d19a66 orange
128
        null: Rgb(86, 182, 194),       // #56b6c2 cyan
129
        deleted_line_bg: None,
130
        deleted_highlight_bg: None,
131
        inserted_line_bg: None,
132
        inserted_highlight_bg: None,
133
        moved_line_bg: None,
134
        moved_highlight_bg: None,
135
    };
136

137
    /// One Dark Pro color theme.
138
    pub const ONE_DARK_PRO: Self = Self {
139
        deleted: Rgb(224, 108, 117),   // #e06c75 red
140
        inserted: Rgb(152, 195, 121),  // #98c379 green
141
        moved: Rgb(97, 175, 239),      // #61afef blue
142
        unchanged: Rgb(171, 178, 191), // #abb2bf white (normal text)
143
        key: Rgb(171, 178, 191),       // #abb2bf white
144
        structure: Rgb(171, 178, 191), // #abb2bf white
145
        comment: Rgb(92, 99, 112),     // #5c6370 gray (muted)
146
        string: Rgb(152, 195, 121),    // #98c379 green
147
        number: Rgb(209, 154, 102),    // #d19a66 orange
148
        boolean: Rgb(209, 154, 102),   // #d19a66 orange
149
        null: Rgb(86, 182, 194),       // #56b6c2 cyan
150
        deleted_line_bg: None,
151
        deleted_highlight_bg: None,
152
        inserted_line_bg: None,
153
        inserted_highlight_bg: None,
154
        moved_line_bg: None,
155
        moved_highlight_bg: None,
156
    };
157

158
    /// Tokyo Night color theme.
159
    pub const TOKYO_NIGHT: Self = Self {
160
        deleted: Rgb(247, 118, 142),   // red
161
        inserted: Rgb(158, 206, 106),  // green
162
        moved: Rgb(122, 162, 247),     // blue
163
        unchanged: Rgb(192, 202, 245), // white (normal text)
164
        key: Rgb(192, 202, 245),       // white
165
        structure: Rgb(192, 202, 245), // white
166
        comment: Rgb(86, 95, 137),     // gray (muted)
167
        string: Rgb(158, 206, 106),    // green
168
        number: Rgb(255, 158, 100),    // orange
169
        boolean: Rgb(255, 158, 100),   // orange
170
        null: Rgb(125, 207, 255),      // cyan
171
        deleted_line_bg: None,
172
        deleted_highlight_bg: None,
173
        inserted_line_bg: None,
174
        inserted_highlight_bg: None,
175
        moved_line_bg: None,
176
        moved_highlight_bg: None,
177
    };
178

179
    /// Get the color for a change kind.
NEW
180
    pub fn color_for(&self, kind: crate::ChangeKind) -> Rgb {
×
NEW
181
        match kind {
×
NEW
182
            crate::ChangeKind::Unchanged => self.unchanged,
×
NEW
183
            crate::ChangeKind::Deleted => self.deleted,
×
NEW
184
            crate::ChangeKind::Inserted => self.inserted,
×
NEW
185
            crate::ChangeKind::MovedFrom | crate::ChangeKind::MovedTo => self.moved,
×
NEW
186
            crate::ChangeKind::Modified => self.deleted, // old value gets deleted color
×
187
        }
NEW
188
    }
×
189

190
    /// Blend two colors in linear sRGB space.
191
    /// `t` ranges from 0.0 (all `a`) to 1.0 (all `b`).
192
    pub fn blend(a: Rgb, b: Rgb, t: f32) -> Rgb {
6✔
193
        // Convert to linear sRGB for perceptually correct blending
194
        let a_lin: LinSrgb =
6✔
195
            Srgb::new(a.0 as f32 / 255.0, a.1 as f32 / 255.0, a.2 as f32 / 255.0).into_linear();
6✔
196
        let b_lin: LinSrgb =
6✔
197
            Srgb::new(b.0 as f32 / 255.0, b.1 as f32 / 255.0, b.2 as f32 / 255.0).into_linear();
6✔
198

199
        // Mix in linear space
200
        let mixed = a_lin.mix(b_lin, t);
6✔
201

202
        // Convert back to sRGB
203
        let result: Srgb = mixed.into();
6✔
204
        Rgb(
6✔
205
            (result.red * 255.0).round() as u8,
6✔
206
            (result.green * 255.0).round() as u8,
6✔
207
            (result.blue * 255.0).round() as u8,
6✔
208
        )
6✔
209
    }
6✔
210

211
    /// Brighten and saturate a color for use in highlights.
212
    /// Increases both lightness and saturation in LCH space.
213
    pub fn brighten_saturate(rgb: Rgb, lightness_boost: f32, chroma_boost: f32) -> Rgb {
6✔
214
        let srgb = Srgb::new(
6✔
215
            rgb.0 as f32 / 255.0,
6✔
216
            rgb.1 as f32 / 255.0,
6✔
217
            rgb.2 as f32 / 255.0,
6✔
218
        );
219
        let mut lch = Lch::from_color(srgb);
6✔
220

221
        // Increase lightness
222
        lch.l = (lch.l + lightness_boost * 100.0).min(100.0);
6✔
223

224
        // Increase chroma (saturation-like)
225
        lch.chroma = (lch.chroma + chroma_boost).min(150.0);
6✔
226

227
        let result: Srgb = Srgb::from_color(lch);
6✔
228
        Rgb(
6✔
229
            (result.red * 255.0).round() as u8,
6✔
230
            (result.green * 255.0).round() as u8,
6✔
231
            (result.blue * 255.0).round() as u8,
6✔
232
        )
6✔
233
    }
6✔
234

235
    /// Desaturate a color for use in backgrounds.
236
    /// Reduces saturation (chroma) in LCH space.
237
    pub fn desaturate(rgb: Rgb, amount: f32) -> Rgb {
19✔
238
        let srgb = Srgb::new(
19✔
239
            rgb.0 as f32 / 255.0,
19✔
240
            rgb.1 as f32 / 255.0,
19✔
241
            rgb.2 as f32 / 255.0,
19✔
242
        );
243
        let mut lch = Lch::from_color(srgb);
19✔
244

245
        // Reduce chroma (saturation)
246
        lch.chroma = lch.chroma * (1.0 - amount);
19✔
247

248
        let result: Srgb = Srgb::from_color(lch);
19✔
249
        Rgb(
19✔
250
            (result.red * 255.0).round() as u8,
19✔
251
            (result.green * 255.0).round() as u8,
19✔
252
            (result.blue * 255.0).round() as u8,
19✔
253
        )
19✔
254
    }
19✔
255

256
    /// Get the key color blended for a deleted context.
NEW
257
    pub fn deleted_key(&self) -> Rgb {
×
NEW
258
        Self::blend(self.key, self.deleted, 0.5)
×
NEW
259
    }
×
260

261
    /// Get the key color blended for an inserted context.
NEW
262
    pub fn inserted_key(&self) -> Rgb {
×
NEW
263
        Self::blend(self.key, self.inserted, 0.5)
×
NEW
264
    }
×
265

266
    /// Get the structure color blended for a deleted context.
267
    pub fn deleted_structure(&self) -> Rgb {
3✔
268
        Self::blend(self.structure, self.deleted, 0.4)
3✔
269
    }
3✔
270

271
    /// Get the structure color blended for an inserted context.
272
    pub fn inserted_structure(&self) -> Rgb {
3✔
273
        Self::blend(self.structure, self.inserted, 0.4)
3✔
274
    }
3✔
275

276
    /// Get the comment color blended for a deleted context.
NEW
277
    pub fn deleted_comment(&self) -> Rgb {
×
NEW
278
        Self::blend(self.comment, self.deleted, 0.35)
×
NEW
279
    }
×
280

281
    /// Get the comment color blended for an inserted context.
NEW
282
    pub fn inserted_comment(&self) -> Rgb {
×
NEW
283
        Self::blend(self.comment, self.inserted, 0.35)
×
NEW
284
    }
×
285

286
    // === Value type blending methods ===
287

288
    /// Get the string color blended for a deleted context.
NEW
289
    pub fn deleted_string(&self) -> Rgb {
×
NEW
290
        Self::blend(self.string, self.deleted, 0.7)
×
NEW
291
    }
×
292

293
    /// Get the string color blended for an inserted context.
NEW
294
    pub fn inserted_string(&self) -> Rgb {
×
NEW
295
        Self::blend(self.string, self.inserted, 0.7)
×
NEW
296
    }
×
297

298
    /// Get the number color blended for a deleted context.
NEW
299
    pub fn deleted_number(&self) -> Rgb {
×
NEW
300
        Self::blend(self.number, self.deleted, 0.7)
×
NEW
301
    }
×
302

303
    /// Get the number color blended for an inserted context.
NEW
304
    pub fn inserted_number(&self) -> Rgb {
×
NEW
305
        Self::blend(self.number, self.inserted, 0.7)
×
NEW
306
    }
×
307

308
    /// Get the boolean color blended for a deleted context.
NEW
309
    pub fn deleted_boolean(&self) -> Rgb {
×
NEW
310
        Self::blend(self.boolean, self.deleted, 0.7)
×
NEW
311
    }
×
312

313
    /// Get the boolean color blended for an inserted context.
NEW
314
    pub fn inserted_boolean(&self) -> Rgb {
×
NEW
315
        Self::blend(self.boolean, self.inserted, 0.7)
×
NEW
316
    }
×
317

318
    /// Get the null color blended for a deleted context.
NEW
319
    pub fn deleted_null(&self) -> Rgb {
×
NEW
320
        Self::blend(self.null, self.deleted, 0.7)
×
NEW
321
    }
×
322

323
    /// Get the null color blended for an inserted context.
NEW
324
    pub fn inserted_null(&self) -> Rgb {
×
NEW
325
        Self::blend(self.null, self.inserted, 0.7)
×
NEW
326
    }
×
327

328
    // === Bright highlight colors for values with highlight backgrounds ===
329

330
    /// Get the string color for a deleted highlight (brightened and saturated accent color).
NEW
331
    pub fn deleted_highlight_string(&self) -> Rgb {
×
NEW
332
        Self::brighten_saturate(self.deleted, 0.15, 0.2)
×
NEW
333
    }
×
334

335
    /// Get the string color for an inserted highlight (brightened and saturated accent color).
NEW
336
    pub fn inserted_highlight_string(&self) -> Rgb {
×
NEW
337
        Self::brighten_saturate(self.inserted, 0.15, 0.2)
×
NEW
338
    }
×
339

340
    /// Get the number color for a deleted highlight (brightened and saturated accent color).
NEW
341
    pub fn deleted_highlight_number(&self) -> Rgb {
×
NEW
342
        Self::brighten_saturate(self.deleted, 0.15, 0.2)
×
NEW
343
    }
×
344

345
    /// Get the number color for an inserted highlight (brightened and saturated accent color).
NEW
346
    pub fn inserted_highlight_number(&self) -> Rgb {
×
NEW
347
        Self::brighten_saturate(self.inserted, 0.15, 0.2)
×
NEW
348
    }
×
349

350
    /// Get the boolean color for a deleted highlight (brightened and saturated accent color).
NEW
351
    pub fn deleted_highlight_boolean(&self) -> Rgb {
×
NEW
352
        Self::brighten_saturate(self.deleted, 0.15, 0.2)
×
NEW
353
    }
×
354

355
    /// Get the boolean color for an inserted highlight (brightened and saturated accent color).
NEW
356
    pub fn inserted_highlight_boolean(&self) -> Rgb {
×
NEW
357
        Self::brighten_saturate(self.inserted, 0.15, 0.2)
×
NEW
358
    }
×
359

360
    /// Get the null color for a deleted highlight (brightened and saturated accent color).
NEW
361
    pub fn deleted_highlight_null(&self) -> Rgb {
×
NEW
362
        Self::brighten_saturate(self.deleted, 0.15, 0.2)
×
NEW
363
    }
×
364

365
    /// Get the null color for an inserted highlight (brightened and saturated accent color).
NEW
366
    pub fn inserted_highlight_null(&self) -> Rgb {
×
NEW
367
        Self::brighten_saturate(self.inserted, 0.15, 0.2)
×
NEW
368
    }
×
369

370
    // === Syntax highlight colors (keys, structure, comments with brightened accents) ===
371

372
    /// Get the key color for a deleted highlight (brightened and saturated accent color).
373
    pub fn deleted_highlight_key(&self) -> Rgb {
1✔
374
        Self::brighten_saturate(self.deleted, 0.15, 0.2)
1✔
375
    }
1✔
376

377
    /// Get the key color for an inserted highlight (brightened and saturated accent color).
378
    pub fn inserted_highlight_key(&self) -> Rgb {
1✔
379
        Self::brighten_saturate(self.inserted, 0.15, 0.2)
1✔
380
    }
1✔
381

382
    /// Get the structure color for a deleted highlight (brightened and saturated accent color).
NEW
383
    pub fn deleted_highlight_structure(&self) -> Rgb {
×
NEW
384
        Self::brighten_saturate(self.deleted, 0.15, 0.2)
×
NEW
385
    }
×
386

387
    /// Get the structure color for an inserted highlight (brightened and saturated accent color).
NEW
388
    pub fn inserted_highlight_structure(&self) -> Rgb {
×
NEW
389
        Self::brighten_saturate(self.inserted, 0.15, 0.2)
×
NEW
390
    }
×
391

392
    /// Get the comment color for a deleted highlight (brightened and saturated accent color).
NEW
393
    pub fn deleted_highlight_comment(&self) -> Rgb {
×
NEW
394
        Self::brighten_saturate(self.deleted, 0.15, 0.2)
×
NEW
395
    }
×
396

397
    /// Get the comment color for an inserted highlight (brightened and saturated accent color).
NEW
398
    pub fn inserted_highlight_comment(&self) -> Rgb {
×
NEW
399
        Self::brighten_saturate(self.inserted, 0.15, 0.2)
×
NEW
400
    }
×
401

402
    // === Desaturated background getters ===
403

404
    /// Get desaturated deleted line background (more saturated ambient, darker context).
405
    pub fn desaturated_deleted_line_bg(&self) -> Option<Rgb> {
8✔
406
        self.deleted_line_bg.map(|bg| Self::desaturate(bg, 0.2))
8✔
407
    }
8✔
408

409
    /// Get desaturated inserted line background (more saturated ambient, darker context).
410
    pub fn desaturated_inserted_line_bg(&self) -> Option<Rgb> {
7✔
411
        self.inserted_line_bg.map(|bg| Self::desaturate(bg, 0.2))
7✔
412
    }
7✔
413

414
    /// Get desaturated moved line background (more saturated ambient, darker context).
NEW
415
    pub fn desaturated_moved_line_bg(&self) -> Option<Rgb> {
×
NEW
416
        self.moved_line_bg.map(|bg| Self::desaturate(bg, 0.2))
×
NEW
417
    }
×
418

419
    /// Get desaturated deleted highlight background (very desaturated, minimal brightness boost).
420
    pub fn desaturated_deleted_highlight_bg(&self) -> Option<Rgb> {
2✔
421
        self.deleted_highlight_bg.map(|bg| {
2✔
422
            let brightened = Self::brighten_saturate(bg, 0.02, -5.0);
2✔
423
            Self::desaturate(brightened, 0.75)
2✔
424
        })
2✔
425
    }
2✔
426

427
    /// Get desaturated inserted highlight background (very desaturated, minimal brightness boost).
428
    pub fn desaturated_inserted_highlight_bg(&self) -> Option<Rgb> {
2✔
429
        self.inserted_highlight_bg.map(|bg| {
2✔
430
            let brightened = Self::brighten_saturate(bg, 0.02, -5.0);
2✔
431
            Self::desaturate(brightened, 0.75)
2✔
432
        })
2✔
433
    }
2✔
434

435
    /// Get desaturated moved highlight background (very desaturated, minimal brightness boost).
NEW
436
    pub fn desaturated_moved_highlight_bg(&self) -> Option<Rgb> {
×
NEW
437
        self.moved_highlight_bg.map(|bg| {
×
NEW
438
            let brightened = Self::brighten_saturate(bg, 0.02, -5.0);
×
NEW
439
            Self::desaturate(brightened, 0.75)
×
NEW
440
        })
×
NEW
441
    }
×
442
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc