feat: show diff hunk scope labels

Show per-side Tree-sitter scope paths inline and as sticky continuation labels.
Keep presentation rows out of source coordinates, selections, and edits.

Verified: cargo test (949 passed), cargo build, and real PTY smoke covering
split/unified, wrapping, both split axes, sticky scrolling, live rename and undo.

Size exception: 549 implementation lines added plus deleted, excluding tests
and docs. Keep syntax extraction and coordinate-safe rendering together as one
working behavior.
This commit is contained in:
Valdemar Lorenzen
2026-10-15 14:15:05 +02:00
parent 75dc31b66f
commit a43094dda3
5 changed files with 958 additions and 68 deletions
+486 -65
View File
@@ -512,6 +512,8 @@ impl HistoryDiff {
position: self.top,
segments,
final_segments,
scope: [false; 2],
unwrapped: false,
});
}
let previous = self.top;
@@ -3635,6 +3637,27 @@ fn draw_diff(
PaneLayout::After => [Rect::default(), panes],
}
};
// Parse complete trees before projecting context; color only visible source rows below.
if let Some(id) = diff.comparison_id
&& let Some(file) = selected.filter(|file| file.loaded)
{
panels
.diff_syntax
.get_or_insert_with(DiffSyntaxCache::default)
.update(
id,
diff.file,
file,
diff.after_source_lines(file),
diff.live_revision,
Some(DiffSyntaxViewport {
diff,
top: diff.top,
areas: [Rect::default(); 2],
wrapped: None,
}),
);
}
let mut document_caret = None;
let mut top = diff.top;
let mut before_left = diff.left_before;
@@ -3652,26 +3675,8 @@ fn draw_diff(
})
})
.and_then(|previous| {
let editor = editor?;
let focus = editor.diff_buffer_focus()?;
if previous.view_mode != diff.view_mode || previous.split_axis != diff.split_axis {
return previous.caret_row;
}
let cursor = editor.cursor();
if previous.wrapping {
let file = selected?;
let viewport = panels.diff_wrap.as_mut()?;
let position = viewport.caret_position(diff, file, focus, cursor)?;
viewport
.rows
.iter()
.position(|row| row.position == position)
} else {
let display = diff.source_display_row(focus, cursor.row)?;
let row = display.checked_sub(previous.viewport?.2)?;
let side = usize::from(focus == Focus::After) + 1;
(row < usize::from(diff_inner(previous.regions[side].0).height)).then_some(row)
}
editor?.diff_buffer_focus()?;
previous.caret_row
});
let center_row = editor.and_then(|editor| {
let focus = editor.diff_buffer_focus()?;
@@ -3693,7 +3698,9 @@ fn draw_diff(
.then_some(height / 2)
});
let toggle_row = center_row.or(toggle_row);
if wrapping && let Some(file) = selected.filter(|file| file.loaded) {
if let Some(file) = selected.filter(|file| file.loaded)
&& let Some(syntax) = &panels.diff_syntax
{
let caret = editor
.filter(|editor| editor.diff_interacting() && diff.follow_caret)
.and_then(|editor| {
@@ -3711,6 +3718,7 @@ fn draw_diff(
caret,
toggle_row,
editor.map_or(Some(0), Editor::scroll_distance),
syntax,
);
top = viewport.top.display;
} else {
@@ -3725,15 +3733,6 @@ fn draw_diff(
Focus::After => (after_area, &mut after_left),
Focus::Files => unreachable!("native diff buffers belong to a text pane"),
};
let height = usize::from(diff_inner(pane).height);
if let Some(row) = diff.source_display_row(focus, editor.cursor().row) {
if let Some(anchor) = toggle_row {
top = row.saturating_sub(anchor.min(height.saturating_sub(1)));
}
if toggle_row.is_none() {
top = scroll_offset(top, row, height, editor.scroll_distance());
}
}
let source_len = selected.map_or(0, |file| match focus {
Focus::Before => file.before.len(),
Focus::After => file.after.len(),
@@ -4063,10 +4062,65 @@ fn draw_diff_pane(
let (top, left, previous_width) = viewport;
let gutter = diff_gutter(diff, file, usize::from(content.width), focus);
let width = usize::from(content.width).saturating_sub(gutter);
if wrapped.is_none() && previous_width.is_some_and(|previous| usize::from(previous) != width) {
if !diff.word_wrap
&& !content_only
&& previous_width.is_some_and(|previous| usize::from(previous) != width)
{
*left = diff.bound_horizontal(focus, *left, width);
}
let left = *left;
if let Some(rows) = wrapped {
for (screen, row) in rows.iter().take(usize::from(content.height)).enumerate() {
if row.scope == [false; 2] {
continue;
}
let side = if unified {
usize::from(!row.scope[0])
} else {
usize::from(!before)
};
if !row.scope[side] {
continue;
}
let source_focus = if side == 0 {
Focus::Before
} else {
Focus::After
};
let Some(scope) = diff
.display_source_row(source_focus, row.position.display)
.and_then(|source| syntax[side].scope_at(source))
else {
continue;
};
let prefix = if width < 4 {
""
} else if !unified {
"› "
} else if row.scope == [true; 2] {
"± "
} else if side == 0 {
"− "
} else {
"+ "
};
let prefix_width = UnicodeWidthStr::width(prefix);
let available = width.saturating_sub(prefix_width);
let label = if UnicodeWidthStr::width(scope.label) > available {
scope.label.rsplit(" › ").next().unwrap_or(scope.label)
} else {
scope.label
};
let style = Style::default()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC);
let x = content.x + gutter as u16;
let y = content.y + screen as u16;
let buffer = frame.buffer_mut();
buffer.set_stringn(x, y, prefix, width, style);
buffer.set_stringn(x + prefix_width as u16, y, label, available, style);
}
}
for (screen, display_row, segments) in diff_screen_rows(diff, top, content.height, wrapped) {
let row = &diff.display_rows[display_row];
let row_area = Rect::new(content.x, content.y + screen as u16, content.width, 1);
@@ -4324,6 +4378,8 @@ struct WrappedDiffRow {
position: DiffWrapPosition,
segments: [Option<crate::wrap::Segment>; 2],
final_segments: [bool; 2],
scope: [bool; 2],
unwrapped: bool,
}
#[derive(Default)]
@@ -4390,6 +4446,15 @@ impl DiffWrapViewport {
) -> WrappedDiffRow {
let mut segments = [None; 2];
let mut final_segments = [false; 2];
if !diff.word_wrap {
return WrappedDiffRow {
position,
segments,
final_segments,
scope: [false; 2],
unwrapped: true,
};
}
if matches!(
diff.display_rows.get(position.display),
Some(DisplayRow::Line(_))
@@ -4413,10 +4478,15 @@ impl DiffWrapViewport {
position,
segments,
final_segments,
scope: [false; 2],
unwrapped: false,
}
}
fn height(&mut self, diff: &DiffState, file: &ChangedFile, display: usize) -> usize {
if !diff.word_wrap {
return 1;
}
if !matches!(diff.display_rows.get(display), Some(DisplayRow::Line(_))) {
return 1;
}
@@ -4462,13 +4532,30 @@ impl DiffWrapViewport {
position
}
fn fill(&mut self, diff: &DiffState, file: &ChangedFile, height: usize) {
fn fill(
&mut self,
diff: &DiffState,
file: &ChangedFile,
height: usize,
syntax: &DiffSyntaxCache,
) {
self.rows.clear();
if diff.display_rows.is_empty() {
return;
}
let mut position = self.top;
for _ in 0..height {
while self.rows.len() < height {
for scope in syntax.labels(diff, position, self.rows.is_empty()) {
if scope.iter().any(|side| *side) && self.rows.len() + 1 < height {
self.rows.push(WrappedDiffRow {
position,
segments: [None; 2],
final_segments: [false; 2],
scope,
unwrapped: false,
});
}
}
let row = self.row(diff, file, position);
self.rows.push(row);
let next = self.step(diff, file, position, true);
@@ -4494,7 +4581,11 @@ impl DiffWrapViewport {
};
let text = source.get(cursor.row).map_or("", String::as_str);
let column = display_column(text, cursor.column);
let continuation = caret_segment(cache.segments(cursor.row, text), column);
let continuation = if diff.word_wrap {
caret_segment(cache.segments(cursor.row, text), column)
} else {
0
};
Some(DiffWrapPosition {
display,
continuation,
@@ -4509,6 +4600,7 @@ impl DiffWrapViewport {
caret: Option<(Focus, Cursor)>,
anchor_row: Option<usize>,
distance: Option<usize>,
syntax: &DiffSyntaxCache,
) {
let Some(id) = diff.comparison_id else { return };
let key = Some((id, diff.file, diff.live_revision));
@@ -4548,7 +4640,7 @@ impl DiffWrapViewport {
let height = usize::from(diff_inner(areas[active]).height);
let paint_height =
usize::from(diff_inner(areas[0]).height.max(diff_inner(areas[1]).height));
self.fill(diff, file, paint_height);
self.fill(diff, file, paint_height, syntax);
if let Some((focus, cursor)) = caret
&& let Some(position) = self.caret_position(diff, file, focus, cursor)
{
@@ -4557,7 +4649,7 @@ impl DiffWrapViewport {
.rows
.iter()
.take(height)
.position(|row| row.position == position);
.position(|row| row.scope == [false; 2] && row.position == position);
let anchor = anchor_row.or_else(|| {
if height == 0 {
None
@@ -4574,10 +4666,28 @@ impl DiffWrapViewport {
});
if let Some(row) = anchor {
self.top = position;
for _ in 0..row.min(height.saturating_sub(1)) {
self.top = self.step(diff, file, self.top, false);
let target = row.min(height.saturating_sub(1));
let label_count = |position, sticky| {
syntax
.labels(diff, position, sticky)
.iter()
.filter(|scope| scope.iter().any(|side| *side))
.count()
.min(height.saturating_sub(1))
};
let mut screen = label_count(self.top, true);
while screen < target {
let previous = self.step(diff, file, self.top, false);
let next_screen =
screen + 1 + label_count(self.top, false) + label_count(previous, true)
- label_count(self.top, true);
if previous == self.top || next_screen > target {
break;
}
self.top = previous;
screen = next_screen;
}
self.fill(diff, file, paint_height);
self.fill(diff, file, paint_height, syntax);
}
}
}
@@ -4592,7 +4702,14 @@ fn diff_screen_rows<'a>(
(0..usize::from(height)).filter_map(move |screen| {
if let Some(rows) = wrapped {
rows.get(screen)
.map(|row| (screen, row.position.display, Some(row.segments)))
.filter(|row| row.scope == [false; 2])
.map(|row| {
(
screen,
row.position.display,
(!row.unwrapped).then_some(row.segments),
)
})
} else {
(top + screen < diff.display_rows.len()).then_some((screen, top + screen, None))
}
@@ -4618,6 +4735,39 @@ struct DiffSyntaxCache {
}
impl DiffSyntaxCache {
fn scopes<'a>(
&'a self,
diff: &DiffState,
display: usize,
) -> [Option<crate::syntax::ScopeContext<'a>>; 2] {
[Focus::Before, Focus::After].map(|focus| {
let source = diff.display_source_row(focus, display)?;
let syntax = if focus == Focus::Before {
&self.before
} else {
&self.after
};
syntax.scope_at(source)
})
}
fn labels(&self, diff: &DiffState, position: DiffWrapPosition, sticky: bool) -> [[bool; 2]; 2] {
let scopes = self.scopes(diff, position.display);
if !sticky
&& (position.continuation > 0
|| position.display > 0 && scopes == self.scopes(diff, position.display - 1))
{
return [[false; 2]; 2];
}
if diff.view_mode == ViewMode::Unified
&& scopes[0].map(|scope| scope.label) != scopes[1].map(|scope| scope.label)
{
[[scopes[0].is_some(), false], [false, scopes[1].is_some()]]
} else {
[[scopes[0].is_some(), scopes[1].is_some()], [false; 2]]
}
}
fn update(
&mut self,
comparison_id: u64,
@@ -4692,12 +4842,15 @@ fn diff_visible_rows(
} else {
pane.height
};
let count = wrapped.map_or(usize::from(height), |rows| {
rows.last().map_or(0, |row| row.position.display - top + 1)
});
for display in top..top.saturating_add(count).min(diff.display_rows.len()) {
for (_, display, _) in diff_screen_rows(diff, top, height, wrapped) {
let source =
diff.display_source_row(if before { Focus::Before } else { Focus::After }, display);
if ranges
.last()
.is_some_and(|range| source.is_some_and(|source| range.contains(&source)))
{
continue;
}
if let Some(source) = source {
if let Some(last) = ranges.last_mut()
&& last.end == source
@@ -7741,6 +7894,85 @@ mod tests {
}
}
#[test]
fn diff_scope_labels_identify_multiple_scopes_and_sticky_renamed_sides() {
let before = "impl Review {\n fn old_name(&self) {\n let first = 1;\n let second = 2;\n }\n fn next(&self) {\n let third = 3;\n }\n}";
let after = before
.replace("old_name", "new_name")
.replace("first = 1", "first = 9");
for axis in [SplitAxis::Vertical, SplitAxis::Horizontal] {
for wrapping in [false, true] {
let mut diff = diff_comparison(vec![syntax_diff_file("scope.rs", before, &after)]);
diff.files_visible = false;
diff.split_axis = axis;
diff.word_wrap = wrapping;
let mut terminal =
Terminal::new(ratatui::backend::TestBackend::new(160, 48)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &diff, &mut panels);
let rows = &panels.diff_wrap.as_ref().unwrap().rows;
for (side, name, other) in
[(0, "old_name", "new_name"), (1, "new_name", "old_name")]
{
let area = diff_inner(panels.diff.as_ref().unwrap().regions[side + 1].0);
let labels = rows
.iter()
.enumerate()
.filter(|(_, row)| row.scope[side])
.map(|(screen, _)| {
(area.x..area.right())
.map(|x| {
terminal.backend().buffer()[(x, area.y + screen as u16)]
.symbol()
})
.collect::<String>()
})
.collect::<Vec<_>>();
assert!(labels.iter().any(|label| label.contains(name)));
assert!(labels.iter().any(|label| label.contains("next")));
assert!(!labels.iter().any(|label| label.contains(other)));
}
let syntax = panels.diff_syntax.as_ref().unwrap();
let parses = (syntax.before.parse_counts(), syntax.after.parse_counts());
diff.top = diff.source_display_row(Focus::Before, 2).unwrap();
render_diff(&mut terminal, &diff, &mut panels);
let rows = &panels.diff_wrap.as_ref().unwrap().rows;
assert_eq!(rows[0].scope, [true; 2]);
assert_eq!(rows[1].scope, [false; 2]);
assert_eq!(rows[0].position, rows[1].position);
let first = diff_screen_rows(&diff, diff.top, 48, Some(rows))
.next()
.unwrap();
assert_eq!((first.0, first.1), (1, diff.top));
let syntax = panels.diff_syntax.as_ref().unwrap();
assert_eq!(
(syntax.before.parse_counts(), syntax.after.parse_counts()),
parses
);
}
}
let mut narrow = diff_comparison(vec![syntax_diff_file("scope.rs", before, &after)]);
narrow.files_visible = false;
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 30)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &narrow, &mut panels);
assert!(screen_text(&terminal).contains("› old_name"));
assert!(screen_text(&terminal).contains("› new_name"));
let diff = diff_comparison(vec![syntax_diff_file("scope.txt", before, &after)]);
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 30)).unwrap();
let mut panels = WheelPanels::default();
render_diff(&mut terminal, &diff, &mut panels);
assert!(
panels
.diff_wrap
.as_ref()
.unwrap()
.rows
.iter()
.all(|row| row.scope == [false; 2])
);
}
#[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();
@@ -7754,11 +7986,19 @@ mod tests {
diff_comparison(vec![syntax_diff_file("working.rs", "fn old() {}", source)]);
diff.files_visible = false;
render_diff(&mut terminal, &diff, &mut panels);
let source_row = panels
.diff_wrap
.as_ref()
.unwrap()
.rows
.iter()
.position(|row| row.scope == [false; 2])
.unwrap() as u16;
let after = panels.diff.as_ref().unwrap().regions[2].0;
let cell = &terminal.backend().buffer()[(after.x + 4, after.y + 1)];
let cell = &terminal.backend().buffer()[(after.x + 4, after.y + 1 + source_row)];
assert_eq!(cell.fg, foreground, "{source}");
let before = panels.diff.as_ref().unwrap().regions[1].0;
let cell = &terminal.backend().buffer()[(before.x + 4, before.y + 1)];
let cell = &terminal.backend().buffer()[(before.x + 4, before.y + 1 + source_row)];
assert_eq!(cell.fg, Color::Magenta);
}
}
@@ -7795,11 +8035,30 @@ mod tests {
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 in [
for (side, region) in [
panels.diff.as_ref().unwrap().regions[1].0,
panels.diff.as_ref().unwrap().regions[2].0,
] {
let cell = &terminal.backend().buffer()[(region.x + 4, region.y + 2)];
]
.into_iter()
.enumerate()
{
let focus = if side == 0 {
Focus::Before
} else {
Focus::After
};
let screen = panels
.diff_wrap
.as_ref()
.unwrap()
.rows
.iter()
.position(|row| {
row.scope == [false; 2]
&& diff.display_source_row(focus, row.position.display) == Some(1)
})
.unwrap();
let cell = &terminal.backend().buffer()[(region.x + 4, region.y + 1 + screen as u16)];
assert_eq!(cell.fg, Color::Magenta);
}
}
@@ -8707,6 +8966,14 @@ mod tests {
let geometry = panels.diff.as_ref().unwrap();
let before = geometry.regions[1].0;
let after = geometry.regions[2].0;
let source_row = panels
.diff_wrap
.as_ref()
.unwrap()
.rows
.iter()
.position(|row| row.scope == [false; 2])
.unwrap() as u16;
let buffer = terminal.backend().buffer();
for pane in [before, after] {
for (column, row, foreground) in [
@@ -8721,32 +8988,70 @@ mod tests {
(23, 2, Color::DarkGray),
] {
assert_eq!(
buffer[(pane.x + 4 + column, pane.y + 1 + row)].fg,
buffer[(pane.x + 4 + column, pane.y + 1 + source_row + 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");
assert_eq!(
buffer[(pane.x + 20, pane.y + 2 + source_row)].symbol(),
"界"
);
assert_eq!(
buffer[(pane.x + 22, pane.y + 2 + source_row)].symbol(),
"e\u{301}"
);
assert_eq!(buffer[(pane.x + 26, pane.y + 3 + source_row)].symbol(), "�");
assert_eq!(buffer[(pane.x + 27, pane.y + 3 + source_row)].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);
assert_eq!(buffer[(x, pane.y + 4 + source_row)].symbol(), " ");
assert_eq!(buffer[(x, pane.y + 4 + source_row)].fg, Color::Reset);
}
}
diff.top = 1;
diff.left_before = 17;
diff.left_after = 16;
render_diff(&mut terminal, &diff, &mut panels);
let source_row = panels
.diff_wrap
.as_ref()
.unwrap()
.rows
.iter()
.position(|row| row.scope == [false; 2])
.unwrap() as u16;
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 + 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 + 6, after.y + 1)].symbol(), "e\u{301}");
assert_eq!(buffer[(after.x + 6, after.y + 1)].fg, Color::Green);
assert_eq!(
buffer[(before.x + 4, before.y + 1 + source_row)].symbol(),
" "
);
assert_eq!(
buffer[(before.x + 4, before.y + 1 + source_row)].fg,
Color::Reset
);
assert_eq!(
buffer[(before.x + 5, before.y + 1 + source_row)].symbol(),
"e\u{301}"
);
assert_eq!(
buffer[(before.x + 5, before.y + 1 + source_row)].fg,
Color::Green
);
assert_eq!(
buffer[(after.x + 4, after.y + 1 + source_row)].symbol(),
"界"
);
assert_eq!(
buffer[(after.x + 4, after.y + 1 + source_row)].fg,
Color::Green
);
assert_eq!(
buffer[(after.x + 6, after.y + 1 + source_row)].symbol(),
"e\u{301}"
);
assert_eq!(
buffer[(after.x + 6, after.y + 1 + source_row)].fg,
Color::Green
);
}
#[test]
@@ -12398,6 +12703,112 @@ end
terminal.get_cursor_position().unwrap()
}
#[test]
fn scope_projection_keeps_unified_wrapped_caret_and_selection_source_only() {
let before = format!(
"fn old_name() {{\n // {}\n}}\nfn second() {{}}\n",
"word ".repeat(45)
);
let after = before.replace("old_name", "new_name");
for axis in [SplitAxis::Vertical, SplitAxis::Horizontal] {
let mut editor = readonly_diff_editor(syntax_diff_file("scope.rs", &before, &after));
if axis == SplitAxis::Horizontal {
editor.execute("diff.orientation.toggle");
}
editor.handle(Input::Char('\t'));
editor.handle(Input::Down);
editor.handle(Input::Home);
editor.handle(Input::Char('v'));
editor.handle(Input::End);
let source = editor.cursor();
let selections = editor.selections();
let text = editor.lines().to_vec();
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 40)).unwrap();
let mut views = BufferViewports::default();
for command in [
"",
"word_wrap.toggle",
"diff.view.toggle",
"word_wrap.toggle",
"diff.view.toggle",
] {
if !command.is_empty() {
editor.execute(command);
}
let caret = render_native_diff(&mut terminal, &mut views, &mut editor);
assert_eq!(editor.cursor(), source);
assert_eq!(editor.selections(), selections);
assert_eq!(editor.lines(), text);
let geometry = views.panels.diff.as_ref().unwrap();
let diff = editor.diff().unwrap();
let active = usize::from(diff.focus == Focus::After);
let area = diff_inner(geometry.regions[active + 1].0);
assert!(area.contains(caret));
let rows = &views.panels.diff_wrap.as_ref().unwrap().rows;
let caret_row = usize::from(caret.y - area.y);
assert_eq!(rows[caret_row].scope, [false; 2]);
assert_eq!(
diff.display_source_row(diff.focus, rows[caret_row].position.display),
Some(source.row)
);
assert!(rows.iter().any(|row| row.scope != [false; 2]));
if diff.view_mode == ViewMode::Unified {
let labels = rows
.iter()
.enumerate()
.filter(|(_, row)| row.scope != [false; 2])
.map(|(screen, _)| {
(area.x..area.right())
.map(|x| {
terminal.backend().buffer()[(x, area.y + screen as u16)]
.symbol()
})
.collect::<String>()
})
.collect::<Vec<_>>();
assert!(labels.iter().any(|label| label.contains("− old_name")));
assert!(labels.iter().any(|label| label.contains("+ new_name")));
}
for (screen, row) in rows.iter().take(usize::from(area.height)).enumerate() {
if row.scope == [false; 2] {
continue;
}
let y = area.y + screen as u16;
for x in area.x..area.right() {
assert_ne!(terminal.backend().buffer()[(x, y)].bg, Color::Blue);
}
for x in area.x..area.right() - geometry.text_widths[active] {
assert_eq!(terminal.backend().buffer()[(x, y)].symbol(), " ");
}
}
}
}
}
#[test]
fn tiny_horizontal_scope_diff_keeps_active_source_visible() {
let mut editor = readonly_diff_editor(syntax_diff_file(
"small.rs",
"fn old_name() {}",
"fn new_name() {}",
));
editor.execute("diff.orientation.toggle");
editor.handle(Input::Char('\t'));
editor.handle(Input::Char('\t'));
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(80, 9)).unwrap();
let mut views = BufferViewports::default();
let caret = render_native_diff(&mut terminal, &mut views, &mut editor);
let area = diff_inner(views.panels.diff.as_ref().unwrap().regions[2].0);
assert!(
area.contains(caret),
"source caret {caret:?} outside {area:?}"
);
assert_eq!(
views.panels.diff_wrap.as_ref().unwrap().rows[usize::from(caret.y - area.y)].scope,
[false; 2]
);
}
#[test]
fn modified_sequences_navigate_diff_files_and_readonly_panes() {
let mut editor = Editor::open(None, vec![]).unwrap();
@@ -12688,7 +13099,17 @@ end
let text = editor.lines().to_vec();
editor.execute("word_wrap.toggle");
render_native_diff(&mut terminal, &mut views, &mut editor);
assert!(views.panels.diff_wrap.is_none());
assert!(!editor.word_wrap());
assert!(
views
.panels
.diff_wrap
.as_ref()
.unwrap()
.rows
.iter()
.all(|row| row.position.continuation == 0)
);
assert_eq!(editor.selections(), selection);
assert_eq!(editor.lines(), text);
assert_eq!(editor.mode(), Mode::Visual);