feat: enable native readonly diff interaction and synchronized pane cursors
This commit is contained in:
+324
-37
@@ -174,13 +174,13 @@ impl BufferViewports {
|
||||
|
||||
fn draw_session(&mut self, frame: &mut Frame, session: &mut EditorSession) {
|
||||
self.draw(frame, session.editor());
|
||||
if let Some((comparison_id, file, top, left_after)) = self
|
||||
if let Some((comparison_id, file, top, left_before, left_after)) = self
|
||||
.panels
|
||||
.diff
|
||||
.as_ref()
|
||||
.and_then(|geometry| geometry.viewport)
|
||||
{
|
||||
session.set_diff_viewport(comparison_id, file, top, left_after);
|
||||
session.set_diff_viewport(comparison_id, file, top, left_before, left_after);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,7 +720,7 @@ fn draw(
|
||||
|
||||
struct DiffGeometry {
|
||||
regions: [(Rect, Focus); 3],
|
||||
viewport: Option<(u64, usize, usize, usize)>,
|
||||
viewport: Option<(u64, usize, usize, usize, usize)>,
|
||||
}
|
||||
|
||||
fn diff_panel(frame: &mut Frame, area: Rect, title: &str, focused: bool) -> Rect {
|
||||
@@ -915,6 +915,7 @@ fn paint_diff_editor(
|
||||
editor: &Editor,
|
||||
diff: &DiffState,
|
||||
area: Rect,
|
||||
focus: Focus,
|
||||
viewport: (usize, usize),
|
||||
) -> Option<Position> {
|
||||
let (top, left) = viewport;
|
||||
@@ -931,12 +932,16 @@ fn paint_diff_editor(
|
||||
let DisplayRow::Line(aligned) = *row else {
|
||||
continue;
|
||||
};
|
||||
let Some(source) = diff.rows[aligned].after else {
|
||||
let Some(source) = (match focus {
|
||||
Focus::Before => diff.rows[aligned].before,
|
||||
Focus::After => diff.rows[aligned].after,
|
||||
Focus::Files => None,
|
||||
}) else {
|
||||
continue;
|
||||
};
|
||||
let line_area = Rect::new(area.x, area.y + (display - top) as u16, area.width, 1);
|
||||
paint_editor_line(frame, editor, source, line_area, left, &carets, &selections);
|
||||
if source == editor.cursor().row && editor.diff_editing() {
|
||||
if source == editor.cursor().row && editor.diff_interacting() {
|
||||
let column = display_column(&editor.lines()[source], editor.cursor().column);
|
||||
if column >= left && column < left.saturating_add(usize::from(area.width)) {
|
||||
cursor = Some(Position::new(area.x + (column - left) as u16, line_area.y));
|
||||
@@ -1243,16 +1248,27 @@ fn draw_diff(
|
||||
display_text(before),
|
||||
display_text(after),
|
||||
diff.focus,
|
||||
editor.filter(|editor| editor.diff_editable()).map_or_else(
|
||||
|| "read-only".into(),
|
||||
|editor| format!(
|
||||
"After editable · {}{} · {}:{}",
|
||||
editor.mode(),
|
||||
if editor.dirty() { " [+]" } else { "" },
|
||||
editor.cursor().row + 1,
|
||||
editor.cursor().column + 1,
|
||||
),
|
||||
)
|
||||
editor
|
||||
.filter(|editor| editor.diff_interacting())
|
||||
.map_or_else(
|
||||
|| "read-only".into(),
|
||||
|editor| format!(
|
||||
"{} · {}{} · {}:{}",
|
||||
if editor.diff_editing() {
|
||||
"editable"
|
||||
} else {
|
||||
"read-only"
|
||||
},
|
||||
if editor.visual_linewise() {
|
||||
"VISUAL LINE".into()
|
||||
} else {
|
||||
editor.mode().to_string()
|
||||
},
|
||||
if editor.dirty() { " [+]" } else { "" },
|
||||
editor.cursor().row + 1,
|
||||
editor.cursor().column + 1,
|
||||
),
|
||||
)
|
||||
))
|
||||
.style(Style::default().fg(Color::Black).bg(Color::Cyan)),
|
||||
header,
|
||||
@@ -1289,25 +1305,42 @@ fn draw_diff(
|
||||
};
|
||||
let mut document_caret = None;
|
||||
let mut top = diff.top;
|
||||
let mut before_left = diff.left_before;
|
||||
let mut after_left = diff.left_after;
|
||||
if let Some(editor) = editor.filter(|editor| {
|
||||
editor.diff_editing() && diff.follow_caret && after_area != Rect::default()
|
||||
}) {
|
||||
let height = usize::from(after_area.height.saturating_sub(2));
|
||||
if let Some(row) = diff.after_display_row(editor.cursor().row) {
|
||||
if let Some(editor) = editor.filter(|editor| editor.diff_interacting() && diff.follow_caret)
|
||||
&& let Some(focus) = editor.diff_buffer_focus()
|
||||
{
|
||||
let (pane, left) = match focus {
|
||||
Focus::Before => (before_area, &mut before_left),
|
||||
Focus::After => (after_area, &mut after_left),
|
||||
Focus::Files => unreachable!("native diff buffers belong to a text pane"),
|
||||
};
|
||||
let height = usize::from(pane.height.saturating_sub(2));
|
||||
if let Some(row) = diff.source_display_row(focus, editor.cursor().row) {
|
||||
if row < top {
|
||||
top = row;
|
||||
} else if height > 0 && row >= top.saturating_add(height) {
|
||||
top = row + 1 - height;
|
||||
}
|
||||
}
|
||||
let digits = editor.lines().len().max(1).ilog10() as usize + 1;
|
||||
let width = usize::from(after_area.width.saturating_sub(2)).saturating_sub(digits + 2);
|
||||
let source_len = selected.map_or(0, |file| match focus {
|
||||
Focus::Before => file.before.len(),
|
||||
Focus::After => file.after.len(),
|
||||
Focus::Files => unreachable!("native buffer belongs to a text pane"),
|
||||
});
|
||||
let digits = source_len.max(1).ilog10() as usize + 1;
|
||||
let content_width = usize::from(pane.width.saturating_sub(2));
|
||||
let gutter = if content_width > digits + 2 {
|
||||
digits + 2
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let width = content_width.saturating_sub(gutter);
|
||||
let column = display_column(&editor.lines()[editor.cursor().row], editor.cursor().column);
|
||||
if column < after_left {
|
||||
after_left = column;
|
||||
} else if width > 0 && column >= after_left.saturating_add(width) {
|
||||
after_left = column + 1 - width;
|
||||
if column < *left {
|
||||
*left = column;
|
||||
} else if width > 0 && column >= left.saturating_add(width) {
|
||||
*left = column + 1 - width;
|
||||
}
|
||||
}
|
||||
if let Some(picker) = &diff.files
|
||||
@@ -1329,15 +1362,28 @@ fn draw_diff(
|
||||
diff_panel(frame, before_area, "", diff.focus == Focus::Before);
|
||||
} else {
|
||||
if before_area != Rect::default() {
|
||||
draw_diff_pane(
|
||||
let text_area = draw_diff_pane(
|
||||
frame,
|
||||
diff,
|
||||
file,
|
||||
before_area,
|
||||
true,
|
||||
&syntax.before,
|
||||
(top, diff.left_before),
|
||||
(top, before_left),
|
||||
);
|
||||
if let Some(editor) =
|
||||
editor.filter(|editor| editor.diff_buffer_focus() == Some(Focus::Before))
|
||||
{
|
||||
document_caret = paint_diff_editor(
|
||||
frame,
|
||||
editor,
|
||||
diff,
|
||||
text_area,
|
||||
Focus::Before,
|
||||
(top, before_left),
|
||||
)
|
||||
.map(|position| (text_area, position));
|
||||
}
|
||||
}
|
||||
if after_area != Rect::default() {
|
||||
let text_area = draw_diff_pane(
|
||||
@@ -1349,10 +1395,18 @@ fn draw_diff(
|
||||
&syntax.after,
|
||||
(top, after_left),
|
||||
);
|
||||
if let Some(editor) = editor.filter(|editor| editor.diff_editable()) {
|
||||
document_caret =
|
||||
paint_diff_editor(frame, editor, diff, text_area, (top, after_left))
|
||||
.map(|position| (text_area, position));
|
||||
if let Some(editor) =
|
||||
editor.filter(|editor| editor.diff_buffer_focus() == Some(Focus::After))
|
||||
{
|
||||
document_caret = paint_diff_editor(
|
||||
frame,
|
||||
editor,
|
||||
diff,
|
||||
text_area,
|
||||
Focus::After,
|
||||
(top, after_left),
|
||||
)
|
||||
.map(|position| (text_area, position));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1368,7 +1422,7 @@ fn draw_diff(
|
||||
}
|
||||
frame.render_widget(
|
||||
Paragraph::new(
|
||||
"File-picker/explorer bindings · Tab focus · ↑/↓ scroll/tree · [/] expand above/below in Normal · :diff.close",
|
||||
"Native buffer bindings · Tab focus · [/] expand above/below in Normal · :diff.close",
|
||||
)
|
||||
.style(Style::default().fg(Color::DarkGray)),
|
||||
help,
|
||||
@@ -1381,7 +1435,7 @@ fn draw_diff(
|
||||
],
|
||||
viewport: diff
|
||||
.comparison_id
|
||||
.map(|id| (id, diff.file, top, after_left)),
|
||||
.map(|id| (id, diff.file, top, before_left, after_left)),
|
||||
});
|
||||
if let Some(picker) = &diff.files
|
||||
&& diff.files_visible
|
||||
@@ -2732,7 +2786,8 @@ mod tests {
|
||||
);
|
||||
assert_eq!(present.x, 0);
|
||||
assert_eq!(present.width, 160);
|
||||
assert_eq!(geometry.viewport.unwrap().3, 2);
|
||||
assert_eq!(geometry.viewport.unwrap().3, 0);
|
||||
assert_eq!(geometry.viewport.unwrap().4, 2);
|
||||
assert_eq!(geometry.regions[if added { 2 } else { 1 }].1, diff.focus);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(present.x, present.y)].fg,
|
||||
@@ -3514,7 +3569,6 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("deleted-lines.txt"));
|
||||
assert!(viewports.document.geometry.is_none());
|
||||
assert_eq!(editor.lines(), original);
|
||||
editor.handle(Input::Escape);
|
||||
assert!(editor.diff().is_some());
|
||||
editor.execute("diff.close");
|
||||
@@ -3523,9 +3577,241 @@ mod tests {
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains("original"));
|
||||
assert!(viewports.document.geometry.is_some());
|
||||
assert_eq!(editor.lines(), original);
|
||||
assert_eq!(editor.cursor(), cursor);
|
||||
}
|
||||
|
||||
fn readonly_diff_editor(file: ChangedFile) -> Editor {
|
||||
let mut editor = Editor::open(None, vec![]).unwrap();
|
||||
editor.set_clipboard_mode("separate").unwrap();
|
||||
editor.execute("diff");
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
editor.apply_diff_result(id, Ok(crate::diff::Outcome::Catalog(diff_catalog())));
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Enter);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Enter);
|
||||
let (id, _) = editor.take_diff_request().unwrap();
|
||||
editor.apply_diff_result(
|
||||
id,
|
||||
Ok(crate::diff::Outcome::Comparison(crate::diff::Comparison {
|
||||
root: std::path::PathBuf::new(),
|
||||
editable_paths: Default::default(),
|
||||
before: "baseline".into(),
|
||||
after: "historic".into(),
|
||||
files: vec![file],
|
||||
})),
|
||||
);
|
||||
editor
|
||||
}
|
||||
|
||||
fn render_native_diff(
|
||||
terminal: &mut Terminal<ratatui::backend::TestBackend>,
|
||||
views: &mut BufferViewports,
|
||||
editor: &mut Editor,
|
||||
) -> Position {
|
||||
terminal.draw(|frame| views.draw(frame, editor)).unwrap();
|
||||
let (id, file, top, before, after) = views.panels.diff.as_ref().unwrap().viewport.unwrap();
|
||||
editor.set_diff_viewport(id, file, top, before, after);
|
||||
terminal.get_cursor_position().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_diff_panes_render_native_caret_visual_search_and_paired_focus() {
|
||||
let mut editor = readonly_diff_editor(syntax_diff_file(
|
||||
"history.rs",
|
||||
"fn old() {}\nlet value = \"old\";",
|
||||
"fn new() {}\nlet value = \"new\";",
|
||||
));
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert!(editor.diff_interacting());
|
||||
assert!(!editor.diff_editing());
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 14)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
let caret = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let before = views.panels.diff.as_ref().unwrap().regions[1].0;
|
||||
assert!(before.contains(caret));
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x, caret.y)].symbol(),
|
||||
"f"
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x, caret.y)].fg,
|
||||
Color::Magenta
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x, caret.y)].bg,
|
||||
Color::Rgb(64, 24, 24)
|
||||
);
|
||||
editor.handle(Input::Char('v'));
|
||||
editor.handle(Input::Right);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x, caret.y)].bg,
|
||||
Color::Blue
|
||||
);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Home);
|
||||
editor.handle(Input::Char('V'));
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x, caret.y + 1)].bg,
|
||||
Color::Blue
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(caret.x + 16, caret.y + 1)].bg,
|
||||
Color::Blue
|
||||
);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Char('/'));
|
||||
for ch in "value".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(terminal.get_cursor_position().unwrap().y, 13);
|
||||
editor.handle(Input::Enter);
|
||||
let searched = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert!(before.contains(searched));
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(searched.x, searched.y)].bg,
|
||||
Color::Yellow
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(searched.x, searched.y)].fg,
|
||||
Color::Black
|
||||
);
|
||||
let before_row = editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.source_display_row(Focus::Before, editor.cursor().row)
|
||||
.unwrap();
|
||||
let column = display_column(&editor.lines()[editor.cursor().row], editor.cursor().column);
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert!(editor.diff_interacting());
|
||||
assert!(!editor.diff_editing());
|
||||
let switched = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let after = views.panels.diff.as_ref().unwrap().regions[2].0;
|
||||
assert!(after.contains(switched));
|
||||
assert_eq!(switched.y, searched.y);
|
||||
assert_eq!(switched.x - after.x, searched.x - before.x);
|
||||
assert_eq!(
|
||||
editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.source_display_row(Focus::After, editor.cursor().row),
|
||||
Some(before_row)
|
||||
);
|
||||
assert_eq!(
|
||||
display_column(&editor.lines()[editor.cursor().row], editor.cursor().column),
|
||||
column
|
||||
);
|
||||
assert_eq!(
|
||||
terminal.backend().buffer()[(switched.x, switched.y)].bg,
|
||||
Color::Rgb(24, 48, 32)
|
||||
);
|
||||
editor.handle(Input::Right);
|
||||
let moved = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
editor.handle(Input::Char('\t'));
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||
let returned = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert!(before.contains(returned));
|
||||
assert_eq!(returned.y, moved.y);
|
||||
assert_eq!(returned.x - before.x, moved.x - after.x);
|
||||
assert_eq!(returned.x, searched.x + 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn readonly_diff_viewport_retains_both_offsets_on_direction_and_focus_reversal() {
|
||||
let before = (0..80)
|
||||
.map(|row| format!("old row {row:03} {}", "x".repeat(100)))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let after = before.replace("old row", "new row");
|
||||
let mut editor = readonly_diff_editor(syntax_diff_file("history.txt", &before, &after));
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(120, 18)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
let mut retained_before_left = 0;
|
||||
for focus in [Focus::Before, Focus::After] {
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.diff().unwrap().focus, focus);
|
||||
assert!(editor.diff_interacting());
|
||||
assert!(!editor.diff_editing());
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
for _ in 0..40 {
|
||||
editor.handle(Input::Down);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
}
|
||||
let top = editor.diff().unwrap().top;
|
||||
assert!(top > 0);
|
||||
let bottom = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
editor.handle(Input::Up);
|
||||
let reversed = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(editor.diff().unwrap().top, top);
|
||||
assert_eq!(reversed.y + 1, bottom.y);
|
||||
while editor
|
||||
.diff()
|
||||
.unwrap()
|
||||
.source_display_row(focus, editor.cursor().row)
|
||||
.unwrap()
|
||||
> top
|
||||
{
|
||||
editor.handle(Input::Up);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert_eq!(editor.diff().unwrap().top, top);
|
||||
}
|
||||
editor.handle(Input::Up);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
assert!(editor.diff().unwrap().top < top);
|
||||
for _ in 0..80 {
|
||||
editor.handle(Input::Right);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
}
|
||||
let left = if focus == Focus::Before {
|
||||
editor.diff().unwrap().left_before
|
||||
} else {
|
||||
editor.diff().unwrap().left_after
|
||||
};
|
||||
assert!(left > 0);
|
||||
let right = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
editor.handle(Input::Left);
|
||||
let reversed = render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let offsets = views.panels.diff.as_ref().unwrap().viewport.unwrap();
|
||||
assert_eq!(
|
||||
if focus == Focus::Before {
|
||||
offsets.3
|
||||
} else {
|
||||
offsets.4
|
||||
},
|
||||
left
|
||||
);
|
||||
assert_eq!(reversed.x + 1, right.x);
|
||||
while editor.cursor().column > left {
|
||||
editor.handle(Input::Left);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
let diff = editor.diff().unwrap();
|
||||
assert_eq!(
|
||||
if focus == Focus::Before {
|
||||
diff.left_before
|
||||
} else {
|
||||
diff.left_after
|
||||
},
|
||||
left
|
||||
);
|
||||
}
|
||||
editor.handle(Input::Left);
|
||||
render_native_diff(&mut terminal, &mut views, &mut editor);
|
||||
if focus == Focus::Before {
|
||||
retained_before_left = editor.diff().unwrap().left_before;
|
||||
assert!(retained_before_left < left);
|
||||
} else {
|
||||
assert!(editor.diff().unwrap().left_after < left);
|
||||
assert_eq!(editor.diff().unwrap().left_before, retained_before_left);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editable_after_renders_live_syntax_caret_visual_selection_and_command_prompt() {
|
||||
let directory = Directory::new();
|
||||
@@ -3807,9 +4093,10 @@ mod tests {
|
||||
assert_eq!(diff.files.as_ref().unwrap().selected, selected);
|
||||
assert!(!diff.files.as_ref().unwrap().focused);
|
||||
assert!(viewports.panels.files.is_none());
|
||||
assert!(viewports.document.geometry.is_none());
|
||||
session.execute("diff.close");
|
||||
assert_eq!(session.editor().lines(), original);
|
||||
assert_eq!(session.editor().cursor(), cursor);
|
||||
assert!(viewports.document.geometry.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user