feat: add syntax-aware diff backgrounds and diagonal gap shading

This commit is contained in:
2026-10-11 01:08:45 +02:00
parent 2f595ca4ca
commit db5d417a17
7 changed files with 529 additions and 77 deletions
+22 -1
View File
@@ -147,6 +147,7 @@ pub struct DiffState {
pub loading: bool,
pub error: Option<String>,
pub comparison: Option<Comparison>,
pub comparison_id: Option<u64>,
pub files: Option<FilePicker>,
pub files_visible: bool,
pub file: usize,
@@ -177,6 +178,7 @@ impl DiffState {
loading: true,
error: None,
comparison: None,
comparison_id: None,
files: None,
files_visible: true,
file: 0,
@@ -215,7 +217,7 @@ impl DiffState {
self.error = None;
}
pub(crate) fn apply_result(&mut self, result: Result<Outcome, String>) {
pub(crate) fn apply_result(&mut self, id: u64, result: Result<Outcome, String>) {
let expected = self.request_kind.take();
self.loading = false;
self.pending = None;
@@ -254,6 +256,7 @@ impl DiffState {
}
self.files = Some(files);
self.comparison = Some(comparison);
self.comparison_id = Some(id);
self.phase = Phase::View;
self.focus = Focus::Files;
self.file = 0;
@@ -1236,6 +1239,24 @@ mod tests {
assert_eq!(editor.diff().unwrap().file, 0);
}
#[test]
fn comparison_identity_comes_from_accepted_requests_not_working_labels() {
let mut editor = editor();
open(&mut editor);
let first = choose_pair(&mut editor);
assert!(editor.apply_diff_result(first, Ok(Outcome::Comparison(comparison()))));
assert_eq!(editor.diff().unwrap().comparison_id, Some(first));
editor.handle(Input::Escape);
open(&mut editor);
assert_eq!(editor.diff().unwrap().comparison_id, None);
let second = choose_pair(&mut editor);
assert!(second > first);
assert!(!editor.apply_diff_result(first, Ok(Outcome::Comparison(comparison()))));
assert_eq!(editor.diff().unwrap().comparison_id, None);
assert!(editor.apply_diff_result(second, Ok(Outcome::Comparison(comparison()))));
assert_eq!(editor.diff().unwrap().comparison_id, Some(second));
}
#[test]
fn discovery_and_fetch_errors_are_retryable_and_stale_responses_cannot_reopen() {
let mut editor = editor();
+1 -1
View File
@@ -1259,7 +1259,7 @@ impl Editor {
let Some(diff) = &mut self.diff else {
return false;
};
diff.apply_result(result);
diff.apply_result(id, result);
true
}
+5
View File
@@ -156,6 +156,11 @@ impl SyntaxCache {
self.error.as_deref()
}
#[cfg(test)]
pub(crate) fn parse_counts(&self) -> (usize, usize) {
(self.parses, self.injection_parses)
}
fn highlight(
&mut self,
syntax: &'static LanguageSpec,
+492 -73
View File
@@ -41,7 +41,7 @@ use crate::{
Diagnostic, DiagnosticSeverity, SemanticToken, SemanticTokenKind, SemanticTokenModifier,
},
session::EditorSession,
syntax::{SyntaxCache, SyntaxKind},
syntax::{SyntaxCache, SyntaxKind, SyntaxSpan},
};
pub const CAPABILITIES: &[&str] = &["text", "styled_text", "diagnostics"];
@@ -427,6 +427,7 @@ fn draw(
);
return;
}
panels.diff_syntax = None;
syntax.update(editor.lines(), editor.path(), editor.text_revision());
if area.width == 0 || area.height == 0 {
return;
@@ -925,6 +926,19 @@ fn draw_diff(
file_navigation_hint: &str,
panels: &mut WheelPanels,
) {
let comparison = diff.comparison.as_ref();
let selected = comparison.and_then(|comparison| comparison.files.get(diff.file));
if diff.phase == Phase::View
&& let Some(id) = diff.comparison_id
&& let Some(file) = selected
{
panels
.diff_syntax
.get_or_insert_with(DiffSyntaxCache::default)
.update(id, diff.file, file);
} else {
panels.diff_syntax = None;
}
if area.width == 0 || area.height == 0 {
return;
}
@@ -1113,13 +1127,15 @@ fn draw_diff(
return;
}
let comparison = diff.comparison.as_ref();
let selected = comparison.and_then(|comparison| comparison.files.get(diff.file));
let syntax_error = panels
.diff_syntax
.as_ref()
.and_then(|syntax| syntax.before.error().or_else(|| syntax.after.error()));
let notice = diff.error.as_deref().or_else(|| {
if diff.loading {
Some("Loading comparison…")
} else {
selected.and_then(|file| file.notice.as_deref())
syntax_error.or_else(|| selected.and_then(|file| file.notice.as_deref()))
}
});
let [header, notice_area, workspace, help] = Layout::vertical([
@@ -1145,7 +1161,7 @@ fn draw_diff(
if let Some(notice) = notice {
frame.render_widget(
Paragraph::new(display_text(notice)).style(Style::default().fg(
if diff.error.is_some() {
if diff.error.is_some() || syntax_error.is_some() {
Color::Red
} else {
Color::Yellow
@@ -1173,9 +1189,11 @@ fn draw_diff(
comparison.map(|comparison| comparison.files.as_slice()),
);
}
if let Some(file) = selected {
draw_diff_pane(frame, diff, file, before_area, true);
draw_diff_pane(frame, diff, file, after_area, false);
if let Some(file) = selected
&& let Some(syntax) = &panels.diff_syntax
{
draw_diff_pane(frame, diff, file, before_area, true, &syntax.before);
draw_diff_pane(frame, diff, file, after_area, false, &syntax.after);
} else {
frame.render_widget(
Paragraph::new(if diff.loading {
@@ -1217,6 +1235,7 @@ fn draw_diff_pane(
file: &ChangedFile,
area: Rect,
before: bool,
syntax: &SyntaxCache,
) {
if area.width == 0 || area.height == 0 {
return;
@@ -1259,62 +1278,94 @@ fn draw_diff_pane(
0
};
let width = usize::from(content.width).saturating_sub(gutter);
let lines: Vec<Line> = diff
for (aligned_row, row) in diff
.rows
.iter()
.enumerate()
.skip(diff.top)
.take(usize::from(content.height))
.map(|row| {
let index = if before { row.before } else { row.after };
let changed = matches!(row.kind, RowKind::Changed)
|| (before && row.kind == RowKind::Removed)
|| (!before && row.kind == RowKind::Added);
{
let row_area = Rect::new(
content.x,
content.y + (aligned_row - diff.top) as u16,
content.width,
1,
);
let index = if before { row.before } else { row.after };
let Some(index) = index else {
let style = Style::default()
.fg(Color::Rgb(72, 72, 72))
.bg(Color::Rgb(28, 28, 28));
let buffer = frame.buffer_mut();
for x in 0..content.width {
let slash = (usize::from(x) % 4 + aligned_row % 4 + left % 4) % 4 == 0;
buffer[(content.x + x, row_area.y)]
.set_symbol(if slash { "/" } else { " " })
.set_style(style);
}
continue;
};
let Some(source_text) = source.get(index) else {
continue;
};
let changed = matches!(row.kind, RowKind::Changed)
|| (before && row.kind == RowKind::Removed)
|| (!before && row.kind == RowKind::Added);
let background = if changed {
if before {
Color::Rgb(64, 24, 24)
} else {
Color::Rgb(24, 48, 32)
}
} else {
Color::Reset
};
let text = if source_text.chars().any(|ch| ch.is_control() && ch != '\t') {
Cow::Owned(
source_text
.chars()
.map(|ch| {
if ch.is_control() && ch != '\t' {
'�'
} else {
ch
}
})
.collect::<String>(),
)
} else {
Cow::Borrowed(source_text.as_str())
};
frame.render_widget(
Paragraph::new(visible_line(&text, left, width))
.style(Style::default().fg(Color::Reset).bg(background)),
Rect::new(content.x + gutter as u16, row_area.y, width as u16, 1),
);
if gutter > 0 {
let marker = if changed {
if before { '-' } else { '+' }
} else {
' '
};
let color = if changed {
if before { Color::Red } else { Color::Green }
} else {
Color::Reset
Color::DarkGray
};
let Some(index) = index else {
return Line::from("");
};
let Some(text) = source.get(index) else {
return Line::from("");
};
let text = if text.chars().any(|ch| ch.is_control() && ch != '\t') {
Cow::Owned(
text.chars()
.map(|ch| {
if ch.is_control() && ch != '\t' {
'�'
} else {
ch
}
})
.collect::<String>(),
)
} else {
Cow::Borrowed(text.as_str())
};
let mut spans = Vec::with_capacity(2);
if gutter > 0 {
let marker = if changed {
if before { '-' } else { '+' }
} else {
' '
};
spans.push(Span::styled(
format!("{:>digits$}{marker} ", index + 1),
Style::default().fg(if changed { color } else { Color::DarkGray }),
));
}
spans.push(Span::styled(
visible_line(&text, left, width),
Style::default().fg(color),
));
Line::from(spans)
})
.collect();
frame.render_widget(Paragraph::new(lines), content);
frame.render_widget(
Paragraph::new(format!("{:>digits$}{marker} ", index + 1))
.style(Style::default().fg(color).bg(background)),
Rect::new(content.x, row_area.y, gutter as u16, 1),
);
}
paint_syntax_line(
frame,
source_text,
syntax.line(index),
Rect::new(content.x + gutter as u16, row_area.y, width as u16, 1),
left,
);
}
}
fn floating_panel(area: Rect) -> Rect {
@@ -1379,6 +1430,34 @@ fn draw_startup_diagnostic(frame: &mut Frame, diagnostic: &StartupDiagnostic, ar
}
}
#[derive(Default)]
struct DiffSyntaxCache {
key: Option<(u64, usize)>,
revision: u64,
before: SyntaxCache,
after: SyntaxCache,
}
impl DiffSyntaxCache {
fn update(&mut self, comparison_id: u64, file_index: usize, file: &ChangedFile) {
let key = Some((comparison_id, file_index));
if self.key != key {
self.key = key;
self.revision = self
.revision
.checked_add(1)
.expect("diff syntax revisions exhausted");
}
self.before.update(
&file.before,
Some(file.old_path.as_deref().unwrap_or(&file.path)),
self.revision,
);
self.after
.update(&file.after, Some(&file.path), self.revision);
}
}
#[derive(Default)]
struct WheelPanels {
completion: Option<Rect>,
@@ -1387,6 +1466,7 @@ struct WheelPanels {
files: Option<(Rect, usize, usize)>,
manual_files: Option<FileWheelOffset>,
diff: Option<DiffGeometry>,
diff_syntax: Option<DiffSyntaxCache>,
}
struct FileWheelOffset {
@@ -2067,6 +2147,45 @@ fn draw_palette(frame: &mut Frame, palette: &Palette, mode: Mode, navigation_hin
);
}
fn paint_syntax_line(
frame: &mut Frame,
source: &str,
spans: &[SyntaxSpan],
area: Rect,
left: usize,
) {
if spans.is_empty() || area.width == 0 || area.height == 0 {
return;
}
let right = left.saturating_add(usize::from(area.width));
let mut span_index = 0;
let mut column = 0;
for (byte, grapheme) in source.grapheme_indices(true) {
if column >= right {
break;
}
let cells = if grapheme == "\t" {
TAB_WIDTH - column % TAB_WIDTH
} else {
UnicodeWidthStr::width(grapheme) + grapheme.chars().filter(|ch| ch.is_control()).count()
};
let end = column + cells;
while span_index < spans.len() && spans[span_index].end_byte <= byte {
span_index += 1;
}
if let Some(span) = spans.get(span_index).filter(|span| span.start_byte <= byte)
&& end > left
&& (grapheme == "\t" || (column >= left && end <= right))
{
let style = syntax_style(span.kind);
for cell in column.max(left)..end.min(right) {
frame.buffer_mut()[(area.x + (cell - left) as u16, area.y)].set_style(style);
}
}
column = end;
}
}
fn syntax_style(kind: SyntaxKind) -> Style {
Style::default().fg(match kind {
SyntaxKind::Comment => Color::DarkGray,
@@ -2192,20 +2311,165 @@ mod tests {
}
}
fn syntax_diff_file(path: &str, before: &str, after: &str) -> ChangedFile {
let before: Vec<String> = before.split('\n').map(str::to_owned).collect();
let after: Vec<String> = after.split('\n').map(str::to_owned).collect();
ChangedFile {
path: path.into(),
old_path: None,
status: FileStatus::Modified,
hunks: vec![crate::diff::Hunk {
before_start: 0,
before_len: before.len(),
after_start: 0,
after_len: after.len(),
}],
before,
after,
notice: None,
}
}
#[test]
fn diff_syntax_refreshes_same_path_working_snapshots_without_live_buffer_styles() {
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
let mut panels = WheelPanels::default();
for (source, foreground) in [
("fn main() {}", Color::Magenta),
("// current snapshot", Color::DarkGray),
("const VALUE: u8 = 3;", Color::Magenta),
] {
let mut diff =
diff_comparison(vec![syntax_diff_file("working.rs", "fn old() {}", source)]);
diff.files_visible = false;
render_diff(&mut terminal, &diff, &mut panels);
let after = panels.diff.as_ref().unwrap().regions[2].0;
let cell = &terminal.backend().buffer()[(after.x + 4, after.y + 1)];
assert_eq!(cell.fg, foreground, "{source}");
assert_eq!(cell.bg, Color::Rgb(24, 48, 32));
let before = panels.diff.as_ref().unwrap().regions[1].0;
let cell = &terminal.backend().buffer()[(before.x + 4, before.y + 1)];
assert_eq!(cell.fg, Color::Magenta);
assert_eq!(cell.bg, Color::Rgb(64, 24, 24));
}
}
#[test]
fn diff_syntax_uses_each_historic_path_shebang_and_injected_language() {
let mut file = syntax_diff_file("new.rs", "return 42", "fn main() {}");
file.old_path = Some("old.py".into());
let mut syntax = DiffSyntaxCache::default();
syntax.update(1, 0, &file);
assert_eq!(syntax.before.language(), Some("Python"));
assert_eq!(syntax.after.language(), Some("Rust"));
assert_eq!(syntax.before.line(0)[0].kind, SyntaxKind::Keyword);
assert_eq!(syntax.after.line(0)[0].kind, SyntaxKind::Keyword);
file.path = "unknown.extension".into();
syntax.update(1, 0, &file);
assert_eq!(syntax.after.language(), None);
assert!(syntax.after.line(0).is_empty());
let mut file = syntax_diff_file(
"script",
"```rust\nfn old() {}\n```",
"#!/usr/bin/env python3\nreturn 42",
);
file.old_path = Some("history.md".into());
let mut diff = diff_comparison(vec![file]);
diff.files_visible = false;
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &diff, &mut panels);
let syntax = panels.diff_syntax.as_ref().unwrap();
assert_eq!(syntax.before.language(), Some("Markdown"));
assert_eq!(syntax.after.language(), Some("Python"));
assert!(syntax.before.parse_counts().1 > 0);
assert_eq!(syntax.before.line(1)[0].kind, SyntaxKind::Keyword);
assert_eq!(syntax.after.line(1)[0].kind, SyntaxKind::Keyword);
for (region, background) in [
(
panels.diff.as_ref().unwrap().regions[1].0,
Color::Rgb(64, 24, 24),
),
(
panels.diff.as_ref().unwrap().regions[2].0,
Color::Rgb(24, 48, 32),
),
] {
let cell = &terminal.backend().buffer()[(region.x + 4, region.y + 2)];
assert_eq!(cell.fg, Color::Magenta);
assert_eq!(cell.bg, background);
}
}
#[test]
fn diff_syntax_parses_selected_snapshots_not_scroll_focus_or_redraw() {
let first = syntax_diff_file("a.rs", "fn old() {}\n// old", "fn main() {}\n// new");
let second = syntax_diff_file("b.rs", "// prior file", "// selected file");
let mut diff = diff_comparison(vec![first, second]);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 12)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &diff, &mut panels);
let syntax = panels.diff_syntax.as_ref().unwrap();
let counts = (syntax.before.parse_counts(), syntax.after.parse_counts());
assert_eq!(counts, ((1, 0), (1, 0)));
for focus in [Focus::Before, Focus::After, Focus::Files] {
diff.focus = focus;
diff.top = 1;
diff.left_before = 1;
diff.left_after = 2;
render_diff(&mut terminal, &diff, &mut panels);
let syntax = panels.diff_syntax.as_ref().unwrap();
assert_eq!(
(syntax.before.parse_counts(), syntax.after.parse_counts()),
counts
);
let after = panels.diff.as_ref().unwrap().regions[2].0;
assert_eq!(
terminal.backend().buffer()[(after.x + 4, after.y + 1)].fg,
Color::DarkGray,
);
}
diff.files
.as_mut()
.unwrap()
.reveal_path(std::path::Path::new("b.rs"));
diff.handle(Input::Enter);
render_diff(&mut terminal, &diff, &mut panels);
let syntax = panels.diff_syntax.as_ref().unwrap();
assert_eq!(syntax.before.parse_counts(), (2, 0));
assert_eq!(syntax.after.parse_counts(), (2, 0));
let after = panels.diff.as_ref().unwrap().regions[2].0;
assert_eq!(
terminal.backend().buffer()[(after.x + 4, after.y + 1)].fg,
Color::DarkGray,
);
let editor = Editor::open(None, vec![]).unwrap();
let mut views = BufferViewports {
panels,
..BufferViewports::default()
};
terminal.draw(|frame| views.draw(frame, &editor)).unwrap();
assert!(views.panels.diff_syntax.is_none());
}
fn diff_comparison(files: Vec<ChangedFile>) -> DiffState {
static NEXT_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(1);
let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let mut diff = DiffState::new();
diff.take_request();
diff.apply_result(Ok(crate::diff::Outcome::Catalog(diff_catalog())));
diff.apply_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
diff.handle(Input::Enter);
diff.handle(Input::Enter);
diff.take_request();
diff.apply_result(Ok(crate::diff::Outcome::Comparison(
crate::diff::Comparison {
diff.apply_result(
id,
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
before: "old revision".into(),
after: "Working checkout".into(),
files,
},
)));
})),
);
diff
}
@@ -2243,7 +2507,7 @@ mod tests {
fn diff_picker_keeps_working_pinned_while_windowing_searching_and_changing_stage() {
let mut diff = DiffState::new();
diff.take_request();
diff.apply_result(Ok(crate::diff::Outcome::Catalog(diff_catalog())));
diff.apply_result(1, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 16)).unwrap();
let mut panels = WheelPanels::default();
diff.handle(Input::End);
@@ -2297,11 +2561,22 @@ mod tests {
let buffer = terminal.backend().buffer();
let y = before.y + 2;
assert_eq!(buffer[(before.x + 4, y)].symbol(), "g");
assert_eq!(buffer[(before.x + 4, y)].fg, Color::Red);
assert_eq!(buffer[(before.x + 4, y)].fg, Color::Reset);
assert_eq!(buffer[(before.x + 2, y)].symbol(), "-");
for x in after.x + 1..after.right() - 1 {
assert_eq!(buffer[(x, y)].symbol(), " ");
for x in before.x + 1..before.right() - 1 {
assert_eq!(buffer[(x, y)].bg, Color::Rgb(64, 24, 24));
assert_eq!(buffer[(x, y + 1)].bg, Color::Reset);
}
let gap_symbols: Vec<_> = (after.x + 1..after.right() - 1)
.map(|x| {
let cell = &buffer[(x, y)];
let column = x - after.x - 1;
assert_eq!(cell.symbol(), [" ", " ", " ", "/"][usize::from(column % 4)]);
assert_eq!(cell.bg, Color::Rgb(28, 28, 28));
assert_eq!(cell.fg, Color::Rgb(72, 72, 72));
cell.symbol().to_owned()
})
.collect();
assert_eq!(buffer[(before.x + 1, y + 1)].symbol(), "3");
assert_eq!(buffer[(after.x + 1, y + 1)].symbol(), "2");
assert_eq!(buffer[(before.x + 4, y + 1)].symbol(), "t");
@@ -2311,20 +2586,22 @@ mod tests {
render_diff(&mut terminal, &diff, &mut panels);
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(before.x + 4, before.y + 1)].symbol(), "g");
assert_eq!(buffer[(after.x + 4, after.y + 1)].symbol(), " ");
for (x, symbol) in (after.x + 1..after.right() - 1).zip(gap_symbols) {
assert_eq!(buffer[(x, after.y + 1)].symbol(), symbol);
}
assert_eq!(buffer[(before.x, before.y)].fg, Color::Cyan);
assert_eq!(buffer[(after.x, after.y)].fg, Color::DarkGray);
assert!(screen_text(&terminal).contains("Before focus"));
}
#[test]
fn diff_added_rows_leave_before_blank_and_show_green_after_numbers() {
let diff = diff_comparison(vec![ChangedFile {
fn diff_added_rows_hatch_missing_before_but_tint_real_empty_after_lines() {
let mut diff = diff_comparison(vec![ChangedFile {
path: "added.txt".into(),
old_path: None,
status: FileStatus::Added,
before: vec![],
after: vec!["first addition".into(), "second addition".into()],
after: vec!["first addition".into(), String::new()],
hunks: vec![crate::diff::Hunk {
before_start: 0,
before_len: 0,
@@ -2343,12 +2620,146 @@ mod tests {
for offset in 0..2 {
let y = before.y + 1 + offset;
for x in before.x + 1..before.right() - 1 {
assert_eq!(buffer[(x, y)].symbol(), " ");
let column = x - before.x - 1;
let cell = &buffer[(x, y)];
let pattern = if offset == 0 {
["/", " ", " ", " "]
} else {
[" ", " ", " ", "/"]
};
assert_eq!(cell.symbol(), pattern[usize::from(column % 4)]);
assert_eq!(cell.bg, Color::Rgb(28, 28, 28));
assert_eq!(cell.fg, Color::Rgb(72, 72, 72));
}
assert_eq!(buffer[(after.x + 1, y)].symbol(), (offset + 1).to_string());
assert_eq!(buffer[(after.x + 2, y)].symbol(), "+");
assert_eq!(buffer[(after.x + 4, y)].fg, Color::Green);
assert_eq!(buffer[(after.x + 2, y)].fg, Color::Green);
assert_eq!(buffer[(after.x + 4, y)].fg, Color::Reset);
for x in after.x + 1..after.right() - 1 {
assert_eq!(buffer[(x, y)].bg, Color::Rgb(24, 48, 32));
if offset == 1 && x >= after.x + 4 {
assert_eq!(buffer[(x, y)].symbol(), " ");
}
}
}
diff.top = 1;
diff.left_before = 1;
render_diff(&mut terminal, &diff, &mut panels);
let buffer = terminal.backend().buffer();
for x in before.x + 1..before.right() - 1 {
let column = x - before.x - 1;
assert_eq!(
buffer[(x, before.y + 1)].symbol(),
[" ", " ", "/", " "][usize::from(column % 4)],
);
}
assert_eq!(buffer[(before.x, before.y + 1)].symbol(), "│");
assert_eq!(buffer[(before.right() - 1, before.y + 1)].symbol(), "│");
}
#[test]
fn diff_changed_backgrounds_preserve_syntax_and_clipped_source_cells() {
let mut diff = diff_comparison(vec![ChangedFile {
path: "changed.rs".into(),
old_path: None,
status: FileStatus::Modified,
before: [
"fn old() {",
"\tlet word = \"界e\u{301}old\";",
"\tlet count = 42; //\u{1b}note",
"",
"}",
]
.map(String::from)
.to_vec(),
after: [
"fn new() {",
"\tlet word = \"界e\u{301}new\";",
"\tlet count = 24; //\u{1b}note",
"",
"}",
]
.map(String::from)
.to_vec(),
hunks: vec![crate::diff::Hunk {
before_start: 0,
before_len: 5,
after_start: 0,
after_len: 5,
}],
notice: None,
}]);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 12)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &diff, &mut panels);
let geometry = panels.diff.as_ref().unwrap();
let before = geometry.regions[1].0;
let after = geometry.regions[2].0;
let buffer = terminal.backend().buffer();
for (pane, background) in [
(before, Color::Rgb(64, 24, 24)),
(after, Color::Rgb(24, 48, 32)),
] {
for row in 0..5 {
// TestBackend emits the wide glyph, not its continuation cells.
let mut x = pane.x + 1;
while x < pane.right() - 1 {
assert_eq!(
buffer[(x, pane.y + 1 + row)].bg,
background,
"pane={pane:?} x={x} row={row} symbol={:?}",
buffer[(x, pane.y + 1 + row)].symbol()
);
x += UnicodeWidthStr::width(buffer[(x, pane.y + 1 + row)].symbol()).max(1)
as u16;
}
}
for (column, row, foreground) in [
(0, 0, Color::Magenta),
(3, 0, Color::LightBlue),
(4, 1, Color::Magenta),
(15, 1, Color::Green),
(16, 1, Color::Green),
(18, 1, Color::Green),
(16, 2, Color::Yellow),
(22, 2, Color::DarkGray),
(23, 2, Color::DarkGray),
] {
assert_eq!(
buffer[(pane.x + 4 + column, pane.y + 1 + row)].fg,
foreground
);
}
assert_eq!(buffer[(pane.x + 20, pane.y + 2)].symbol(), "界");
assert_eq!(buffer[(pane.x + 22, pane.y + 2)].symbol(), "e\u{301}");
assert_eq!(buffer[(pane.x + 26, pane.y + 3)].symbol(), "�");
assert_eq!(buffer[(pane.x + 27, pane.y + 3)].symbol(), "n");
for x in pane.x + 4..pane.right() - 1 {
assert_eq!(buffer[(x, pane.y + 4)].symbol(), " ");
assert_eq!(buffer[(x, pane.y + 4)].fg, Color::Reset);
}
}
diff.top = 1;
diff.left_before = 17;
diff.left_after = 16;
render_diff(&mut terminal, &diff, &mut panels);
let buffer = terminal.backend().buffer();
assert_eq!(buffer[(before.x + 4, before.y + 1)].symbol(), " ");
assert_eq!(buffer[(before.x + 4, before.y + 1)].fg, Color::Reset);
assert_eq!(
buffer[(before.x + 4, before.y + 1)].bg,
Color::Rgb(64, 24, 24)
);
assert_eq!(buffer[(before.x + 5, before.y + 1)].symbol(), "e\u{301}");
assert_eq!(buffer[(before.x + 5, before.y + 1)].fg, Color::Green);
assert_eq!(buffer[(after.x + 4, after.y + 1)].symbol(), "界");
assert_eq!(buffer[(after.x + 4, after.y + 1)].fg, Color::Green);
assert_eq!(
buffer[(after.x + 4, after.y + 1)].bg,
Color::Rgb(24, 48, 32)
);
assert_eq!(buffer[(after.x + 6, after.y + 1)].symbol(), "e\u{301}");
assert_eq!(buffer[(after.x + 6, after.y + 1)].fg, Color::Green);
}
#[test]
@@ -2387,7 +2798,15 @@ mod tests {
assert_eq!(buffer[(before.x + 6, before.y + 1)].symbol(), "e\u{301}");
assert_eq!(buffer[(after.x + 4, after.y + 1)].symbol(), " ");
assert_eq!(buffer[(after.x + 8, after.y + 1)].symbol(), "界");
assert_eq!(buffer[(after.x + 8, after.y + 1)].fg, Color::Green);
assert_eq!(buffer[(after.x + 8, after.y + 1)].fg, Color::Reset);
assert_eq!(
buffer[(after.x + 8, after.y + 1)].bg,
Color::Rgb(24, 48, 32)
);
assert_eq!(
buffer[(before.x + 4, before.y + 1)].bg,
Color::Rgb(64, 24, 24)
);
diff.handle(Input::Char('\t'));
render_diff(&mut terminal, &diff, &mut panels);
assert!(screen_text(&terminal).contains("After focus"));
@@ -2597,7 +3016,7 @@ mod tests {
render_diff(&mut terminal, &diff, &mut panels);
assert!(screen_text(&terminal).contains("Loading repository"));
diff.take_request();
diff.apply_result(Err("permission denied".into()));
diff.apply_result(1, Err("permission denied".into()));
render_diff(&mut terminal, &diff, &mut panels);
assert!(screen_text(&terminal).contains("permission denied"));
let empty = diff_comparison(vec![]);