fix: overlay clipped scope labels without reflow
This commit is contained in:
+212
-59
@@ -3774,7 +3774,6 @@ fn draw_diff(
|
||||
let viewport = panels
|
||||
.diff_wrap
|
||||
.get_or_insert_with(DiffWrapViewport::default);
|
||||
viewport.lefts = [before_left, after_left];
|
||||
viewport.update(
|
||||
diff,
|
||||
file,
|
||||
@@ -3958,6 +3957,22 @@ fn draw_diff(
|
||||
content,
|
||||
);
|
||||
}
|
||||
if let (Some(file), Some(syntax), Some(viewport)) = (
|
||||
selected.filter(|file| file.loaded),
|
||||
panels.diff_syntax.as_ref(),
|
||||
panels.diff_wrap.as_ref(),
|
||||
) {
|
||||
draw_diff_scope_labels(
|
||||
frame,
|
||||
diff,
|
||||
file,
|
||||
syntax,
|
||||
viewport,
|
||||
[before_area, after_area],
|
||||
[before_left, after_left],
|
||||
&mut document_caret,
|
||||
);
|
||||
}
|
||||
panels.diff = Some(DiffGeometry {
|
||||
regions: [
|
||||
(navigator.unwrap_or_default(), Focus::Files),
|
||||
@@ -4090,6 +4105,116 @@ fn diff_pane_panel(
|
||||
diff_panel(frame, area, &title, access, &revision, diff.focus == focus)
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_diff_scope_labels(
|
||||
frame: &mut Frame,
|
||||
diff: &DiffState,
|
||||
file: &ChangedFile,
|
||||
syntax: &DiffSyntaxCache,
|
||||
viewport: &DiffWrapViewport,
|
||||
areas: [Rect; 2],
|
||||
lefts: [usize; 2],
|
||||
caret: &mut Option<(Rect, Position)>,
|
||||
) {
|
||||
let unified = diff.view_mode == ViewMode::Unified;
|
||||
for (pane, focus) in [Focus::Before, Focus::After].into_iter().enumerate() {
|
||||
if areas[pane].is_empty() || (unified && (diff.focus == Focus::Before) != (pane == 0)) {
|
||||
continue;
|
||||
}
|
||||
let content = diff_inner(areas[pane]);
|
||||
let gutter = diff_gutter(diff, file, usize::from(content.width), focus);
|
||||
let width = usize::from(content.width).saturating_sub(gutter);
|
||||
for (screen, row) in viewport
|
||||
.rows
|
||||
.iter()
|
||||
.take(usize::from(content.height))
|
||||
.enumerate()
|
||||
{
|
||||
let overlay = row.scope == [false; 2];
|
||||
if overlay && !row.unwrapped {
|
||||
continue;
|
||||
}
|
||||
let contexts = syntax.scopes(diff, row.position.display);
|
||||
let side = if !unified {
|
||||
pane
|
||||
} else if overlay {
|
||||
usize::from(diff.display_focus(row.position.display) != Some(Focus::Before))
|
||||
} else {
|
||||
usize::from(!row.scope[0])
|
||||
};
|
||||
let Some(scope) = contexts[side] else {
|
||||
continue;
|
||||
};
|
||||
let y = content.y + screen as u16;
|
||||
let y = if overlay {
|
||||
let source_focus = if side == 0 {
|
||||
Focus::Before
|
||||
} else {
|
||||
Focus::After
|
||||
};
|
||||
if diff.display_source_row(source_focus, row.position.display)
|
||||
!= Some(scope.name.row)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let source = if side == 0 { &file.before } else { &file.after };
|
||||
let prefix = &source[scope.name.row][..scope.name.column];
|
||||
let column = display_column(prefix, usize::MAX);
|
||||
if lefts[pane] <= column && column < lefts[pane].saturating_add(width) {
|
||||
continue;
|
||||
}
|
||||
let Some(y) = y.checked_sub(1).filter(|&y| y >= areas[pane].y) else {
|
||||
continue;
|
||||
};
|
||||
y
|
||||
} else {
|
||||
if !row.scope[side] {
|
||||
continue;
|
||||
}
|
||||
y
|
||||
};
|
||||
let shared = if overlay {
|
||||
contexts[0].map(|scope| scope.label) == contexts[1].map(|scope| scope.label)
|
||||
} else {
|
||||
row.scope == [true; 2]
|
||||
};
|
||||
let prefix = if width < 4 {
|
||||
""
|
||||
} else if !unified {
|
||||
"› "
|
||||
} else if shared {
|
||||
"± "
|
||||
} 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 buffer = frame.buffer_mut();
|
||||
for x in content.x..content.right() {
|
||||
buffer[(x, y)].reset();
|
||||
}
|
||||
buffer.set_stringn(x, y, prefix, width, style);
|
||||
buffer.set_stringn(x + prefix_width as u16, y, label, available, style);
|
||||
if caret.is_some_and(|(_, position)| {
|
||||
position.y == y && content.x <= position.x && position.x < content.right()
|
||||
}) {
|
||||
*caret = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn draw_diff_pane(
|
||||
frame: &mut Frame,
|
||||
@@ -4142,58 +4267,6 @@ fn draw_diff_pane(
|
||||
*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);
|
||||
@@ -4463,7 +4536,6 @@ struct DiffWrapViewport {
|
||||
before: crate::wrap::WrapCache,
|
||||
after: crate::wrap::WrapCache,
|
||||
widths: [usize; 2],
|
||||
lefts: [usize; 2],
|
||||
heights: [usize; 2],
|
||||
visible_names: [Vec<Point>; 2],
|
||||
top: DiffWrapPosition,
|
||||
@@ -4628,13 +4700,13 @@ impl DiffWrapViewport {
|
||||
{
|
||||
return false;
|
||||
}
|
||||
let (left, right) = if row.unwrapped {
|
||||
(self.lefts[side], self.lefts[side] + self.widths[side])
|
||||
} else if let Some(segment) = row.segments[side] {
|
||||
(segment.start, segment.end)
|
||||
} else {
|
||||
if row.unwrapped {
|
||||
return true;
|
||||
}
|
||||
let Some(segment) = row.segments[side] else {
|
||||
return false;
|
||||
};
|
||||
let (left, right) = (segment.start, segment.end);
|
||||
left <= column && column < right
|
||||
})
|
||||
}
|
||||
@@ -8189,6 +8261,87 @@ mod tests {
|
||||
assert_eq!(viewport.rows[0].scope, [false, true]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_scope_horizontal_scroll_keeps_source_rows_fixed() {
|
||||
let before =
|
||||
"keep_previous_code();\nfn review() {\n old_call_with_a_long_argument_name();\n}\n";
|
||||
let after = before.replace("old_call", "new_call");
|
||||
for (mode, axis, focus) in [
|
||||
(ViewMode::Split, SplitAxis::Vertical, Focus::After),
|
||||
(ViewMode::Split, SplitAxis::Horizontal, Focus::After),
|
||||
(ViewMode::Unified, SplitAxis::Vertical, Focus::Before),
|
||||
(ViewMode::Unified, SplitAxis::Vertical, Focus::After),
|
||||
] {
|
||||
let mut file = syntax_diff_file("scope.rs", before, &after);
|
||||
file.hunks = crate::diff::line_hunks(&file.before, false, &file.after, false);
|
||||
let mut diff = diff_comparison(vec![file]);
|
||||
diff.files_visible = false;
|
||||
diff.split_axis = axis;
|
||||
diff.set_view_mode(mode);
|
||||
diff.focus = focus;
|
||||
let declaration = diff.source_display_row(Focus::After, 1).unwrap();
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 30)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
// A declaration at the top uses the border rather than displacing its first row.
|
||||
for top in [0, declaration] {
|
||||
diff.top = top;
|
||||
let mut original = None;
|
||||
for lefts in [[0, 0], [8, 0], [0, 8], [8, 8], [0, 0]] {
|
||||
[diff.left_before, diff.left_after] = lefts;
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let viewport = panels.diff_wrap.as_ref().unwrap();
|
||||
let source_rows = viewport
|
||||
.rows
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, row)| row.scope == [false; 2])
|
||||
.map(|(screen, row)| (screen, row.position))
|
||||
.collect::<Vec<_>>();
|
||||
if let Some(original) = &original {
|
||||
assert_eq!(
|
||||
&source_rows, original,
|
||||
"Horizontal scrolling must not move source rows: {mode:?}, {axis:?}"
|
||||
);
|
||||
} else {
|
||||
original = Some(source_rows);
|
||||
}
|
||||
let screen = viewport
|
||||
.screen_row(DiffWrapPosition {
|
||||
display: declaration,
|
||||
continuation: 0,
|
||||
})
|
||||
.unwrap();
|
||||
for (pane, left) in lefts.into_iter().enumerate() {
|
||||
if mode == ViewMode::Unified && (focus == Focus::Before) != (pane == 0) {
|
||||
continue;
|
||||
}
|
||||
let area = diff_inner(panels.diff.as_ref().unwrap().regions[pane + 1].0);
|
||||
let above = pane_screen(
|
||||
&terminal,
|
||||
Rect::new(area.x, area.y + screen as u16 - 1, area.width, 1),
|
||||
);
|
||||
assert_eq!(
|
||||
above.contains("review"),
|
||||
left > 0,
|
||||
"Clipped names belong above the declaration, without adding a row"
|
||||
);
|
||||
if left > 0 {
|
||||
assert_eq!(
|
||||
above.trim(),
|
||||
if mode == ViewMode::Unified {
|
||||
"± review"
|
||||
} else {
|
||||
"› review"
|
||||
},
|
||||
"The overlay must clear source text and its line number"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_scope_labels_follow_declaration_visibility() {
|
||||
let before = "@decorate()\nclass PolicyActiveStatusEnum(BaseEnum):\n ACTIVE = 1\n EXPIRED = 2\n FUTURE = 3\n UNKNOWN = 4\n";
|
||||
|
||||
Reference in New Issue
Block a user