feat: enable native readonly diff interaction and synchronized pane cursors
This commit is contained in:
+70
-18
@@ -690,21 +690,69 @@ impl DiffState {
|
||||
true
|
||||
}
|
||||
|
||||
pub fn after_display_row(&self, source_row: usize) -> Option<usize> {
|
||||
pub(crate) fn source_display_row(&self, focus: Focus, source_row: usize) -> Option<usize> {
|
||||
self.display_rows.iter().position(|display| {
|
||||
matches!(*display, DisplayRow::Line(index) if self.rows[index].after == Some(source_row))
|
||||
matches!(*display, DisplayRow::Line(index) if self.source_row(focus, index) == Some(source_row))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn after_rows(&self) -> impl DoubleEndedIterator<Item = usize> + '_ {
|
||||
self.excerpt_ranges
|
||||
.iter()
|
||||
.flat_map(|&(start, end)| self.rows[start..end].iter().filter_map(|row| row.after))
|
||||
pub fn after_display_row(&self, source_row: usize) -> Option<usize> {
|
||||
self.source_display_row(Focus::After, source_row)
|
||||
}
|
||||
|
||||
pub(crate) fn clamp_after_row(&self, row: usize) -> Option<usize> {
|
||||
fn source_row(&self, focus: Focus, index: usize) -> Option<usize> {
|
||||
match focus {
|
||||
Focus::Before => self.rows[index].before,
|
||||
Focus::After => self.rows[index].after,
|
||||
Focus::Files => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn source_rows(&self, focus: Focus) -> impl DoubleEndedIterator<Item = usize> + '_ {
|
||||
self.excerpt_ranges.iter().flat_map(move |&(start, end)| {
|
||||
(start..end).filter_map(move |index| self.source_row(focus, index))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn snapshot_lines(&self, focus: Focus) -> Vec<String> {
|
||||
let file = &self.comparison.as_ref().expect("snapshot comparison").files[self.file];
|
||||
let (source, eof, live) = match focus {
|
||||
Focus::Before => (&file.before, file.before_eof, false),
|
||||
Focus::After => (
|
||||
&file.after,
|
||||
file.after_eof,
|
||||
self.live_files.contains(&self.file),
|
||||
),
|
||||
Focus::Files => unreachable!("snapshot belongs to a text pane"),
|
||||
};
|
||||
let mut lines = source.clone();
|
||||
if !live && (eof || lines.is_empty()) {
|
||||
lines.push(String::new());
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
pub(crate) fn corresponding_row(&self, from: Focus, to: Focus, row: usize) -> Option<usize> {
|
||||
let aligned = self.rows.iter().position(|aligned| match from {
|
||||
Focus::Before => aligned.before == Some(row),
|
||||
Focus::After => aligned.after == Some(row),
|
||||
Focus::Files => false,
|
||||
})?;
|
||||
self.display_rows
|
||||
.iter()
|
||||
.filter_map(|display| match *display {
|
||||
DisplayRow::Line(index) => self
|
||||
.source_row(to, index)
|
||||
.map(|row| (index.abs_diff(aligned), row)),
|
||||
DisplayRow::Fold { .. } => None,
|
||||
})
|
||||
.min_by_key(|&(distance, row)| (distance, row))
|
||||
.map(|(_, row)| row)
|
||||
}
|
||||
|
||||
pub(crate) fn clamp_source_row(&self, focus: Focus, row: usize) -> Option<usize> {
|
||||
let mut previous = None;
|
||||
for visible in self.after_rows() {
|
||||
for visible in self.source_rows(focus) {
|
||||
if visible >= row {
|
||||
return Some(match previous {
|
||||
Some(previous) if row - previous < visible - row => previous,
|
||||
@@ -1007,8 +1055,8 @@ impl DiffState {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn expand_after_excerpt(&mut self, below: bool, source_row: usize) {
|
||||
let Some(display_row) = self.after_display_row(source_row) else {
|
||||
pub(crate) fn expand_source_excerpt(&mut self, focus: Focus, below: bool, source_row: usize) {
|
||||
let Some(display_row) = self.source_display_row(focus, source_row) else {
|
||||
return;
|
||||
};
|
||||
self.expand_excerpt_at(below, self.display_rows[display_row]);
|
||||
@@ -1983,17 +2031,21 @@ mod tests {
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Right);
|
||||
assert_eq!(editor.diff().unwrap().top, 1);
|
||||
assert_eq!(editor.diff().unwrap().left_before, 1);
|
||||
assert_eq!(editor.cursor().row, 1);
|
||||
assert_eq!(editor.cursor().column, 1);
|
||||
editor.scroll_diff(Focus::Before, true);
|
||||
editor.set_diff_viewport(id, 0, 1, 1, 0);
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::After);
|
||||
editor.handle(Input::Ctrl('j'));
|
||||
editor.handle(Input::Ctrl('l'));
|
||||
assert_eq!(editor.diff().unwrap().top, 2);
|
||||
editor.handle(Input::Down);
|
||||
editor.handle(Input::Right);
|
||||
assert_eq!(editor.cursor().row, 2);
|
||||
assert_eq!(editor.diff().unwrap().top, 1);
|
||||
editor.set_diff_viewport(id, 0, 2, 1, 1);
|
||||
assert_eq!(editor.diff().unwrap().left_after, 1);
|
||||
assert_eq!(editor.diff().unwrap().left_before, 1);
|
||||
editor.handle(Input::End);
|
||||
assert_eq!(editor.diff().unwrap().top, 5);
|
||||
assert_eq!(editor.diff().unwrap().top, 2);
|
||||
editor.handle(Input::BackTab);
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||
editor.handle(Input::BackTab);
|
||||
@@ -2136,12 +2188,12 @@ mod tests {
|
||||
] {
|
||||
editor.handle(input);
|
||||
}
|
||||
assert_eq!(editor.context().text, original);
|
||||
assert_eq!(editor.carets(), carets);
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Escape);
|
||||
assert!(editor.diff().is_some());
|
||||
editor.execute("diff.close");
|
||||
assert_eq!(editor.context().text, original);
|
||||
assert_eq!(editor.carets(), carets);
|
||||
editor.handle(Input::Char('u'));
|
||||
assert_eq!(editor.context().text, "original\\nsecond");
|
||||
}
|
||||
|
||||
+837
-199
File diff suppressed because it is too large
Load Diff
@@ -44,6 +44,8 @@ Buffers: :edit PATH, :buffers, :buffer N, :buffer_next, :buffer_previous, :buffe
|
||||
Markdown links: :follow_link (primary caret; local files only)
|
||||
Diff: :diff (Normal); choose Before then After; Working checkout After edits live unsaved buffers
|
||||
Before and historical/remote After stay read-only; Tab/Shift+Tab categories/Normal focus
|
||||
All text panes use native motions, selections and copy; read-only panes reject edits/saves
|
||||
Switching Before/After preserves the aligned cursor row and display column
|
||||
Type to search IDs/refs; navigation skips omitted lines without expanding excerpts
|
||||
Normal [/] explicitly expands context; Insert brackets remain literal text
|
||||
:diff.close exits explicitly; Esc cancels modes/prompts/filtering without closing the diff
|
||||
|
||||
+19
-11
@@ -263,10 +263,11 @@ impl EditorSession {
|
||||
comparison_id: u64,
|
||||
file: usize,
|
||||
top: usize,
|
||||
left_before: usize,
|
||||
left_after: usize,
|
||||
) {
|
||||
self.editor
|
||||
.set_diff_viewport(comparison_id, file, top, left_after);
|
||||
.set_diff_viewport(comparison_id, file, top, left_before, left_after);
|
||||
}
|
||||
|
||||
pub(crate) fn scroll_diff(&mut self, focus: Focus, down: bool) {
|
||||
@@ -405,7 +406,7 @@ impl EditorSession {
|
||||
return;
|
||||
}
|
||||
let context = self.editor.context();
|
||||
let read_only = self.editor.diff().is_some() && !self.editor.diff_editing();
|
||||
let read_only = self.editor.mutation_blocked();
|
||||
let result = match self.plugins.get_mut(&invocation.plugin) {
|
||||
Some(plugin) if plugin.is_async() => {
|
||||
let requests = self.requests.entry(invocation.plugin.clone()).or_default();
|
||||
@@ -483,7 +484,7 @@ impl EditorSession {
|
||||
}
|
||||
let mut events = Vec::new();
|
||||
if !self.editor.should_quit() {
|
||||
for document in self.editor.documents() {
|
||||
for document in self.editor.interaction_documents() {
|
||||
let path = document.path.map(Path::to_string_lossy);
|
||||
let previous = self.documents.get(&document.id);
|
||||
let renamed = previous
|
||||
@@ -576,7 +577,7 @@ impl EditorSession {
|
||||
|
||||
fn source_is_current(&self, source: InvocationSource) -> bool {
|
||||
self.editor
|
||||
.documents()
|
||||
.interaction_documents()
|
||||
.any(|document| document.id == source.buffer_id && document.revision == source.revision)
|
||||
}
|
||||
|
||||
@@ -586,7 +587,7 @@ impl EditorSession {
|
||||
request_id: Option<u64>,
|
||||
source: InvocationSource,
|
||||
) -> bool {
|
||||
if self.editor.diff().is_some() && !self.editor.diff_editing() {
|
||||
if self.editor.diff().is_some() && !self.editor.diff_interacting() {
|
||||
return false;
|
||||
}
|
||||
self.editor.buffer_id() == source.buffer_id
|
||||
@@ -622,6 +623,7 @@ impl EditorSession {
|
||||
|
||||
fn completion_is_current(&self, request_id: Option<u64>, source: InvocationSource) -> bool {
|
||||
self.pending_completion
|
||||
&& !self.editor.mutation_blocked()
|
||||
&& self.request_is_current("core", request_id, source)
|
||||
&& self.editor.mode() == Mode::Insert
|
||||
&& self.editor.palette().is_none()
|
||||
@@ -855,7 +857,7 @@ impl EditorSession {
|
||||
}
|
||||
return Ok(changed);
|
||||
}
|
||||
if self.editor.diff().is_some() && !self.editor.diff_editing() {
|
||||
if self.editor.mutation_blocked() {
|
||||
return Ok(false);
|
||||
}
|
||||
let changed = !effects.is_empty();
|
||||
@@ -3425,29 +3427,35 @@ end
|
||||
.apply_response("fixture", 2, vec![Effect::Insert("stale".into())])
|
||||
.unwrap()
|
||||
);
|
||||
let current = InvocationSource {
|
||||
session.editor.scroll_diff(Focus::Before, false);
|
||||
let readonly = InvocationSource {
|
||||
buffer_id: session.editor.buffer_id(),
|
||||
revision: session.editor.text_revision(),
|
||||
};
|
||||
session.editor.scroll_diff(Focus::Before, false);
|
||||
session
|
||||
.requests
|
||||
.get_mut("fixture")
|
||||
.unwrap()
|
||||
.pending
|
||||
.insert(3, current);
|
||||
.insert(3, readonly);
|
||||
assert!(
|
||||
!session
|
||||
.apply_response("fixture", 3, vec![Effect::Insert("readonly".into())])
|
||||
.unwrap()
|
||||
);
|
||||
assert_eq!(session.editor.lines(), ["acceptedbase"]);
|
||||
assert_eq!(session.editor.lines(), ["base"]);
|
||||
assert!(
|
||||
session
|
||||
.editor
|
||||
.documents()
|
||||
.any(|document| document.lines == ["acceptedbase"])
|
||||
);
|
||||
session
|
||||
.requests
|
||||
.get_mut("fixture")
|
||||
.unwrap()
|
||||
.pending
|
||||
.insert(4, current);
|
||||
.insert(4, readonly);
|
||||
assert!(
|
||||
session
|
||||
.apply_response("fixture", 4, vec![Effect::Message("allowed".into())])
|
||||
|
||||
+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