feat: anchor review comments to precise text ranges
This commit is contained in:
+265
-24
@@ -4396,6 +4396,25 @@ fn draw_diff_pane(
|
||||
&& comment.anchor.path == *path
|
||||
&& diff.review_anchor_current(&comment.anchor)
|
||||
});
|
||||
let selected_anchor = review_comments
|
||||
.clone()
|
||||
.find(|comment| diff.local_review.selected == Some(comment.id))
|
||||
.map(|comment| &comment.anchor);
|
||||
let review_source_row = |side, display| {
|
||||
diff.display_source_row(side, display).or_else(|| {
|
||||
let DisplayRow::Line(index) = diff.display_rows[display] else {
|
||||
return None;
|
||||
};
|
||||
let row = &diff.rows[index];
|
||||
(unified && row.kind == RowKind::Equal)
|
||||
.then_some(match side {
|
||||
Focus::Before => row.before,
|
||||
Focus::After => row.after,
|
||||
Focus::Files => None,
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
};
|
||||
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);
|
||||
@@ -4566,6 +4585,34 @@ fn draw_diff_pane(
|
||||
};
|
||||
paint_trailing_whitespace(frame, &text, text_area, left);
|
||||
}
|
||||
if let Some(anchor) = selected_anchor {
|
||||
let side = match anchor.side {
|
||||
crate::review::Side::Before => Focus::Before,
|
||||
crate::review::Side::After => Focus::After,
|
||||
};
|
||||
if let Some(source_row) = review_source_row(side, display_row)
|
||||
.filter(|source_row| anchor.rows().contains(source_row))
|
||||
{
|
||||
let start = if source_row == anchor.start.row {
|
||||
anchor.start.column
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let end = if source_row == anchor.end.row {
|
||||
anchor.end.column
|
||||
} else {
|
||||
usize::MAX
|
||||
};
|
||||
paint_grapheme_range(
|
||||
frame,
|
||||
text_area,
|
||||
left,
|
||||
source_text,
|
||||
start..end,
|
||||
Style::default().bg(Color::Magenta),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if gutter > 0 {
|
||||
for comment in review_comments {
|
||||
@@ -4575,23 +4622,9 @@ fn draw_diff_pane(
|
||||
};
|
||||
let selected = diff.local_review.selected == Some(comment.id);
|
||||
for (screen, display, _) in diff_screen_rows(diff, top, content.height, wrapped) {
|
||||
let source = diff.display_source_row(side, display).or_else(|| {
|
||||
let DisplayRow::Line(index) = diff.display_rows[display] else {
|
||||
return None;
|
||||
};
|
||||
let row = &diff.rows[index];
|
||||
(unified && row.kind == RowKind::Equal)
|
||||
.then_some(match side {
|
||||
Focus::Before => row.before,
|
||||
Focus::After => row.after,
|
||||
Focus::Files => None,
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
let source = review_source_row(side, display);
|
||||
if wrapped.is_some_and(|rows| rows[screen].position.continuation != 0)
|
||||
|| !source.is_some_and(|row| {
|
||||
(comment.anchor.start_line..=comment.anchor.end_line).contains(&(row + 1))
|
||||
})
|
||||
|| !source.is_some_and(|row| comment.anchor.rows().contains(&row))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -9349,8 +9382,14 @@ mod tests {
|
||||
|
||||
fn commented_diff() -> DiffState {
|
||||
use crate::review::{Anchor, Comment, Side};
|
||||
let before = format!("{}\nold second\nkeep", "old ".repeat(24));
|
||||
let after = format!("{}\nnew second\nkeep", "new ".repeat(24));
|
||||
let before = format!(
|
||||
"{}\nold e\u{301}界 second\nkeep",
|
||||
"old e\u{301}界 ".repeat(12)
|
||||
);
|
||||
let after = format!(
|
||||
"{}\nnew e\u{301}界 second\nkeep",
|
||||
"new e\u{301}界 ".repeat(12)
|
||||
);
|
||||
let mut file = syntax_diff_file("new.txt", &before, &after);
|
||||
file.hunks =
|
||||
crate::diff::line_hunks(&file.before, file.before_eof, &file.after, file.after_eof);
|
||||
@@ -9365,8 +9404,8 @@ mod tests {
|
||||
revision: "before-snapshot".into(),
|
||||
path: "old.txt".into(),
|
||||
side: Side::Before,
|
||||
start_line: 1,
|
||||
end_line: 2,
|
||||
start: crate::plugin::TextPosition { row: 0, column: 0 },
|
||||
end: crate::plugin::TextPosition { row: 2, column: 0 },
|
||||
content: before,
|
||||
},
|
||||
body: "Before range".into(),
|
||||
@@ -9377,8 +9416,8 @@ mod tests {
|
||||
revision: "after-snapshot".into(),
|
||||
path: "new.txt".into(),
|
||||
side: Side::After,
|
||||
start_line: 1,
|
||||
end_line: 1,
|
||||
start: crate::plugin::TextPosition { row: 0, column: 0 },
|
||||
end: crate::plugin::TextPosition { row: 1, column: 0 },
|
||||
content: after,
|
||||
},
|
||||
body: "After line".into(),
|
||||
@@ -9388,6 +9427,155 @@ mod tests {
|
||||
diff
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_review_highlights_exact_graphemes_newlines_clips_and_wrap_continuations() {
|
||||
use crate::plugin::TextPosition;
|
||||
use crate::review::Side;
|
||||
|
||||
for mode in [ViewMode::Split, ViewMode::Unified] {
|
||||
for side in [Side::Before, Side::After] {
|
||||
for (wrapping, horizontal) in [(false, 0), (false, 6), (true, 0)] {
|
||||
for (start, end) in [((0, 4), (0, 6)), ((0, 5), (1, 5)), ((0, 84), (1, 0))] {
|
||||
let mut diff = commented_diff();
|
||||
diff.set_view_mode(mode);
|
||||
diff.top = 0;
|
||||
diff.word_wrap = wrapping;
|
||||
diff.left_before = horizontal;
|
||||
diff.left_after = horizontal;
|
||||
let comment = diff
|
||||
.local_review
|
||||
.document
|
||||
.comments
|
||||
.iter_mut()
|
||||
.find(|comment| comment.anchor.side == side)
|
||||
.unwrap();
|
||||
comment.anchor.start = TextPosition {
|
||||
row: start.0,
|
||||
column: start.1,
|
||||
};
|
||||
comment.anchor.end = TextPosition {
|
||||
row: end.0,
|
||||
column: end.1,
|
||||
};
|
||||
let anchor = comment.anchor.clone();
|
||||
diff.local_review.selected = Some(comment.id);
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(90, 40)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
let file = &diff.comparison.as_ref().unwrap().files[0];
|
||||
let wrapped = panels
|
||||
.diff_wrap
|
||||
.as_ref()
|
||||
.map(|viewport| viewport.rows.as_slice());
|
||||
let buffer = terminal.backend().buffer();
|
||||
for (area, focus) in
|
||||
panels.diff.as_ref().unwrap().regions.into_iter().skip(1)
|
||||
{
|
||||
let content = diff_inner(area);
|
||||
let gutter = diff_gutter(&diff, file, content.width as usize, focus);
|
||||
let width = content.width as usize - gutter;
|
||||
for (screen, display, segments) in
|
||||
diff_screen_rows(&diff, 0, content.height, wrapped)
|
||||
{
|
||||
let mut expected = vec![false; width];
|
||||
if let DisplayRow::Line(aligned) = diff.display_rows[display] {
|
||||
let row = &diff.rows[aligned];
|
||||
let actual_side = if mode == ViewMode::Unified {
|
||||
diff.display_focus(display).unwrap_or(focus)
|
||||
} else {
|
||||
focus
|
||||
};
|
||||
let anchor_side = if side == Side::Before {
|
||||
Focus::Before
|
||||
} else {
|
||||
Focus::After
|
||||
};
|
||||
if actual_side == anchor_side
|
||||
|| (mode == ViewMode::Unified && row.kind == RowKind::Equal)
|
||||
{
|
||||
let source_row = if side == Side::Before {
|
||||
row.before
|
||||
} else {
|
||||
row.after
|
||||
};
|
||||
if let Some(source_row) = source_row {
|
||||
let line = if side == Side::Before {
|
||||
&file.before[source_row]
|
||||
} else {
|
||||
&file.after[source_row]
|
||||
};
|
||||
let segment = segments.and_then(|segments| {
|
||||
segments[usize::from(actual_side == Focus::After)]
|
||||
});
|
||||
let left =
|
||||
segment.map_or(horizontal, |segment| segment.start);
|
||||
let paint_width = segment.map_or(width, |segment| {
|
||||
segment_paint_width(
|
||||
segment,
|
||||
wrapped.unwrap()[screen].final_segments
|
||||
[usize::from(actual_side == Focus::After)],
|
||||
width,
|
||||
)
|
||||
});
|
||||
for column in 0..=line.graphemes(true).count() {
|
||||
let position = TextPosition {
|
||||
row: source_row,
|
||||
column,
|
||||
};
|
||||
let cell_start = display_column(line, column);
|
||||
let cell_end =
|
||||
if column == line.graphemes(true).count() {
|
||||
cell_start + 1
|
||||
} else {
|
||||
display_column(line, column + 1)
|
||||
};
|
||||
if anchor.start <= position
|
||||
&& position < anchor.end
|
||||
&& cell_start >= left
|
||||
&& cell_end <= left + paint_width
|
||||
{
|
||||
expected[cell_start - left..cell_end - left]
|
||||
.fill(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut continuation = 0;
|
||||
for (column, expected) in expected.into_iter().enumerate() {
|
||||
if continuation > 0 {
|
||||
continuation -= 1;
|
||||
continue;
|
||||
}
|
||||
continuation = UnicodeWidthStr::width(
|
||||
buffer[(
|
||||
content.x + gutter as u16 + column as u16,
|
||||
content.y + screen as u16,
|
||||
)]
|
||||
.symbol(),
|
||||
)
|
||||
.saturating_sub(1);
|
||||
assert_eq!(
|
||||
buffer[(
|
||||
content.x + gutter as u16 + column as u16,
|
||||
content.y + screen as u16,
|
||||
)]
|
||||
.bg
|
||||
== Color::Magenta,
|
||||
expected,
|
||||
"{mode:?} {side:?} wrap={wrapping} left={horizontal} \
|
||||
{start:?}..{end:?}, screen={screen} column={column}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_review_markers_preserve_diff_signs_ranges_and_wrapped_geometry() {
|
||||
for mode in [ViewMode::Split, ViewMode::Unified] {
|
||||
@@ -9472,6 +9660,15 @@ mod tests {
|
||||
.all(|cell| !matches!(cell.symbol(), "●" | "○")),
|
||||
"{mode:?} {mismatch}"
|
||||
);
|
||||
assert!(
|
||||
terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.all(|cell| cell.bg != Color::Magenta),
|
||||
"{mode:?} {mismatch}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9486,8 +9683,8 @@ mod tests {
|
||||
.comments
|
||||
.retain(|comment| comment.anchor.side == side);
|
||||
let comment = &mut diff.local_review.document.comments[0];
|
||||
comment.anchor.start_line = 3;
|
||||
comment.anchor.end_line = 3;
|
||||
comment.anchor.start = crate::plugin::TextPosition { row: 2, column: 1 };
|
||||
comment.anchor.end = crate::plugin::TextPosition { row: 2, column: 3 };
|
||||
diff.local_review.selected = Some(comment.id);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(90, 20)).unwrap();
|
||||
render_diff(&mut terminal, &diff, &mut WheelPanels::default());
|
||||
@@ -9496,6 +9693,20 @@ mod tests {
|
||||
"{side:?}: {}",
|
||||
screen_text(&terminal)
|
||||
);
|
||||
let buffer = terminal.backend().buffer();
|
||||
let marker = buffer
|
||||
.content
|
||||
.iter()
|
||||
.position(|cell| cell.symbol() == "●")
|
||||
.unwrap();
|
||||
for offset in 1..=4 {
|
||||
assert_eq!(
|
||||
buffer.content[marker + offset].bg == Color::Magenta,
|
||||
(2..=3).contains(&offset),
|
||||
"{side:?}: source column {}",
|
||||
offset - 1
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14096,6 +14307,36 @@ end
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn native_visual_selection_overrides_selected_review_range_background() {
|
||||
for unified in [false, true] {
|
||||
let mut editor = readonly_diff_editor(syntax_diff_file(
|
||||
"file.txt",
|
||||
"old e\u{301}界 text",
|
||||
"new e\u{301}界 text",
|
||||
));
|
||||
if unified {
|
||||
editor.execute("diff.view.toggle");
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor.execute("diff.comment");
|
||||
editor.handle(Input::Escape);
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 20)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
let source = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(source.x, source.y)].bg,
|
||||
Color::Magenta
|
||||
);
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Right);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let buffer = terminal.backend().buffer();
|
||||
assert_eq!(buffer[(source.x, source.y)].bg, Color::Blue);
|
||||
assert!(buffer.content.iter().any(|cell| cell.bg == Color::Magenta));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_editor_floats_over_the_diff_without_changing_workspace_layout() {
|
||||
for unified in [false, true] {
|
||||
|
||||
Reference in New Issue
Block a user