fix: preserve diff backgrounds and align file markers
This commit is contained in:
+3
-1
@@ -406,7 +406,7 @@ Use the ordinary **file-picker/file-explorer actions and bindings**: Space `o` o
|
|||||||
|
|
||||||
File rows show colored **A** (added, green), **M** (modified, yellow), **D** (removed, red), or **R** (renamed, cyan) markers in Tree and List views, including filtered results. Rename rows show `R old/path -> new/path`; directories have no file-status marker. Ordinary file exploration remains unannotated.
|
File rows show colored **A** (added, green), **M** (modified, yellow), **D** (removed, red), or **R** (renamed, cyan) markers in Tree and List views, including filtered results. Rename rows show `R old/path -> new/path`; directories have no file-status marker. Ordinary file exploration remains unannotated.
|
||||||
|
|
||||||
Mark or unmark a diff file with **`c` while browsing the changed-file tree**, **`:diff.review.toggle`**, or the command-palette action. With the changed-file explorer focused, it toggles the highlighted file without opening it; from a text pane, the command toggles the displayed file. Reviewed files show a green **`✓` immediately before A/M/D/R** in Tree and List views. Directories and ordinary filesystem pickers cannot be marked. While filtering, including List view, `c` remains query text; use the palette or a control-key binding there. Explicit bindings take precedence over the default browsing shortcut.
|
Mark or unmark a diff file with **`c` while browsing the changed-file tree**, **`:diff.review.toggle`**, or the command-palette action. With the changed-file explorer focused, it toggles the highlighted file without opening it; from a text pane, the command toggles the displayed file. Reviewed files show a green **`✓` in the picker gutter** in Tree and List views. A/M/D/R begins at the tree's indentation column, without extra review-marker padding before the status. Directories and ordinary filesystem pickers cannot be marked. While filtering, including List view, `c` remains query text; use the palette or a control-key binding there. Explicit bindings take precedence over the default browsing shortcut.
|
||||||
|
|
||||||
Review marks belong only to the open comparison and survive filtering, file navigation, lazy loading, tabs, split/unified/orientation changes, and pinned checkout-only transitions. A new or recomputed comparison clears them. Editing a writable After file clears only that file's mark; unchanged refreshes and saves preserve it. Marks do not change document text, undo history, or repository state, and are not persisted after closing the comparison.
|
Review marks belong only to the open comparison and survive filtering, file navigation, lazy loading, tabs, split/unified/orientation changes, and pinned checkout-only transitions. A new or recomputed comparison clears them. Editing a writable After file clears only that file's mark; unchanged refreshes and saves preserve it. Marks do not change document text, undo history, or repository state, and are not persisted after closing the comparison.
|
||||||
|
|
||||||
@@ -424,6 +424,8 @@ Diff views inherit the originating buffer's word-wrap setting. `word_wrap.toggle
|
|||||||
|
|
||||||
In split diffs, changed text rows use muted red backgrounds in Before and muted green backgrounds in After, including blank tails and real empty lines. Entirely added or deleted files retain the normal background; colored line numbers and `+`/`-` gutter markers indicate their changes. Foreground colors use the editor's bundled Tree-sitter syntax highlighting, parsed independently for each snapshot and its path (including the old path for renames). Native text panes also display their buffer's selections, search matches, semantic highlighting, and diagnostics. Unknown languages retain the normal foreground; historical snapshots do not borrow live-buffer LSP tokens.
|
In split diffs, changed text rows use muted red backgrounds in Before and muted green backgrounds in After, including blank tails and real empty lines. Entirely added or deleted files retain the normal background; colored line numbers and `+`/`-` gutter markers indicate their changes. Foreground colors use the editor's bundled Tree-sitter syntax highlighting, parsed independently for each snapshot and its path (including the old path for renames). Native text panes also display their buffer's selections, search matches, semantic highlighting, and diagnostics. Unknown languages retain the normal foreground; historical snapshots do not borrow live-buffer LSP tokens.
|
||||||
|
|
||||||
|
The row tint also covers the line-number gutter on wrapped continuations and blank wrap-padding rows on the shorter side of an aligned pair. Empty source lines keep the same full-row tint; missing source counterparts retain their normal-background hatching.
|
||||||
|
|
||||||
Character-wise highlighting is enabled automatically within paired changed lines: only changed graphemes receive a stronger red/green background, while common text retains the muted row tint. Combining sequences, wide characters, and expanded tabs keep their source geometry through horizontal scrolling and word wrap. Syntax/semantic foregrounds remain intact, and cursor/search/selection styles keep their normal precedence. Character ranges are cached with aligned rows; stable live edits refresh only the edited paired lines rather than computing character diffs during painting.
|
Character-wise highlighting is enabled automatically within paired changed lines: only changed graphemes receive a stronger red/green background, while common text retains the muted row tint. Combining sequences, wide characters, and expanded tabs keep their source geometry through horizontal scrolling and word wrap. Syntax/semantic foregrounds remain intact, and cursor/search/selection styles keep their normal precedence. Character ranges are cached with aligned rows; stable live edits refresh only the edited paired lines rather than computing character diffs during painting.
|
||||||
|
|
||||||
Diff backgrounds blend the terminal's default background with its red/green palette colors: an 18% accent for changed rows and a stronger 38% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint, on focus regain, and whenever a subscribed DEC 2031 color-scheme notification arrives. While a diff is visible in any pane, a nonblocking one-second palette probe also catches external theme changes without a notification, including changes between two themes with the same light/dark preference. Ordinary editor views do not poll; cached colors apply across editor panes. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.
|
Diff backgrounds blend the terminal's default background with its red/green palette colors: an 18% accent for changed rows and a stronger 38% accent for changed characters, both computed from the original background. Read-only OSC 11/4 queries run after the initial paint, on focus regain, and whenever a subscribed DEC 2031 color-scheme notification arrives. While a diff is visible in any pane, a nonblocking one-second palette probe also catches external theme changes without a notification, including changes between two themes with the same light/dark preference. Ordinary editor views do not poll; cached colors apply across editor panes. If the complete palette is unavailable within 250 ms, change backgrounds remain normal and gutter colors/markers still identify changes.
|
||||||
|
|||||||
+74
-78
@@ -3319,9 +3319,6 @@ fn draw_diff_pane(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let segment = segments.and_then(|segments| segments[usize::from(!before)]);
|
let segment = segments.and_then(|segments| segments[usize::from(!before)]);
|
||||||
if segments.is_some() && segment.is_none() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let left = segment.map_or(left, |segment| segment.start);
|
let left = segment.map_or(left, |segment| segment.start);
|
||||||
let text_width = segment.map_or(width, |segment| segment.end - segment.start);
|
let text_width = segment.map_or(width, |segment| segment.end - segment.start);
|
||||||
let paint_width = segment.map_or(width, |segment| {
|
let paint_width = segment.map_or(width, |segment| {
|
||||||
@@ -3339,6 +3336,12 @@ fn draw_diff_pane(
|
|||||||
} else {
|
} else {
|
||||||
Color::Reset
|
Color::Reset
|
||||||
};
|
};
|
||||||
|
frame
|
||||||
|
.buffer_mut()
|
||||||
|
.set_style(row_area, Style::default().bg(background));
|
||||||
|
if segments.is_some() && segment.is_none() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
frame.render_widget(
|
frame.render_widget(
|
||||||
Paragraph::new(visible_text(source_text, left, text_width.min(width), true))
|
Paragraph::new(visible_text(source_text, left, text_width.min(width), true))
|
||||||
.style(Style::default().fg(Color::Reset).bg(background)),
|
.style(Style::default().fg(Color::Reset).bg(background)),
|
||||||
@@ -4635,27 +4638,25 @@ fn draw_file_list(
|
|||||||
let old_path = changed
|
let old_path = changed
|
||||||
.and_then(|file| file.old_path.as_deref())
|
.and_then(|file| file.old_path.as_deref())
|
||||||
.or(entry.old_path);
|
.or(entry.old_path);
|
||||||
let prefix = format!(
|
let mut spans = Vec::with_capacity(8);
|
||||||
"{} {:indent$}",
|
let reviewed = changed.is_some()
|
||||||
if selected { ">" } else { " " },
|
&& entry.catalog_index.is_some_and(|index| {
|
||||||
"",
|
|
||||||
indent = if picker.view == FileView::Tree {
|
|
||||||
entry.depth * 2
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let mut spans = Vec::with_capacity(if status.is_some() { 7 } else { 3 });
|
|
||||||
spans.push(Span::raw(prefix));
|
|
||||||
if changed.is_some() {
|
|
||||||
let reviewed = entry.catalog_index.is_some_and(|index| {
|
|
||||||
changed_files.is_some_and(|(_, reviewed)| reviewed.contains(&index))
|
changed_files.is_some_and(|(_, reviewed)| reviewed.contains(&index))
|
||||||
});
|
});
|
||||||
|
spans.push(Span::raw(if selected { ">" } else { " " }));
|
||||||
|
if changed_files.is_some() {
|
||||||
spans.push(Span::styled(
|
spans.push(Span::styled(
|
||||||
if reviewed { "✓ " } else { " " },
|
if reviewed { "✓" } else { " " },
|
||||||
Style::default().fg(Color::Green),
|
Style::default().fg(Color::Green),
|
||||||
));
|
));
|
||||||
|
} else {
|
||||||
|
spans.push(Span::raw(" "));
|
||||||
}
|
}
|
||||||
|
spans.push(Span::raw(if picker.view == FileView::Tree {
|
||||||
|
" ".repeat(entry.depth * 2)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
}));
|
||||||
if let Some(status) = status {
|
if let Some(status) = status {
|
||||||
let color = match status {
|
let color = match status {
|
||||||
FileStatus::Added => Color::Green,
|
FileStatus::Added => Color::Green,
|
||||||
@@ -6825,6 +6826,44 @@ mod tests {
|
|||||||
assert_eq!(diff.focus, Focus::Before);
|
assert_eq!(diff.focus, Focus::Before);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn diff_row_tint_covers_blank_lines_and_wrapped_gutters() {
|
||||||
|
for wrapped in [false, true] {
|
||||||
|
let mut diff = diff_comparison(vec![syntax_diff_file(
|
||||||
|
"changed.txt",
|
||||||
|
"old old old old old old old old old old old old old old old old old old old old\n",
|
||||||
|
"new\n",
|
||||||
|
)]);
|
||||||
|
diff.files_visible = false;
|
||||||
|
diff.word_wrap = wrapped;
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(60, 20)).unwrap();
|
||||||
|
let mut panels = WheelPanels {
|
||||||
|
diff_colors: DiffColors::new(Some(LIGHT_DIFF_THEME)),
|
||||||
|
..WheelPanels::default()
|
||||||
|
};
|
||||||
|
render_diff(&mut terminal, &diff, &mut panels);
|
||||||
|
for side in 0..2 {
|
||||||
|
let area = diff_inner(panels.diff.as_ref().unwrap().regions[side + 1].0);
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
let empty = (area.y..area.bottom())
|
||||||
|
.find(|&y| buffer[(area.x, y)].symbol() == "2")
|
||||||
|
.unwrap();
|
||||||
|
for y in area.y..=empty {
|
||||||
|
for x in area.x..area.x + 3 {
|
||||||
|
assert_eq!(
|
||||||
|
buffer[(x, y)].bg,
|
||||||
|
panels.diff_colors.row[side],
|
||||||
|
"{wrapped} {side} {x} {y}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for x in area.x..area.right() {
|
||||||
|
assert_eq!(buffer[(x, empty)].bg, panels.diff_colors.row[side]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diff_added_rows_hatch_missing_before_but_tint_real_empty_after_lines() {
|
fn diff_added_rows_hatch_missing_before_but_tint_real_empty_after_lines() {
|
||||||
let mut diff = diff_comparison(vec![ChangedFile {
|
let mut diff = diff_comparison(vec![ChangedFile {
|
||||||
@@ -7152,14 +7191,26 @@ mod tests {
|
|||||||
.unwrap_or_else(|| panic!("missing {expected}"));
|
.unwrap_or_else(|| panic!("missing {expected}"));
|
||||||
assert_eq!(buffer[(x, y)].fg, color);
|
assert_eq!(buffer[(x, y)].fg, color);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer[(x - 2, y)].symbol(),
|
buffer[(2, y)].symbol(),
|
||||||
if catalog_index == index { "✓" } else { " " }
|
if catalog_index == index { "✓" } else { " " }
|
||||||
);
|
);
|
||||||
assert_eq!(buffer[(x - 1, y)].symbol(), " ");
|
let depth = picker
|
||||||
if catalog_index == index {
|
.entries()
|
||||||
assert_eq!(buffer[(x - 2, y)].fg, Color::Green);
|
.find(|entry| entry.catalog_index == Some(catalog_index))
|
||||||
|
.unwrap()
|
||||||
|
.depth;
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
buffer[(x - 2, y)].bg,
|
x,
|
||||||
|
3 + if view == FileView::Tree {
|
||||||
|
depth as u16 * 2
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if catalog_index == index {
|
||||||
|
assert_eq!(buffer[(2, y)].fg, Color::Green);
|
||||||
|
assert_eq!(
|
||||||
|
buffer[(2, y)].bg,
|
||||||
if focused {
|
if focused {
|
||||||
Color::Blue
|
Color::Blue
|
||||||
} else {
|
} else {
|
||||||
@@ -7206,7 +7257,6 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let text = screen_text(&terminal);
|
let text = screen_text(&terminal);
|
||||||
assert!(text.contains("✓ M "));
|
|
||||||
assert_eq!(text.matches('✓').count(), 1);
|
assert_eq!(text.matches('✓').count(), 1);
|
||||||
assert!(!text.contains("R r.txt"));
|
assert!(!text.contains("R r.txt"));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -7234,60 +7284,6 @@ mod tests {
|
|||||||
assert!(!screen_text(&terminal).contains('✓'));
|
assert!(!screen_text(&terminal).contains('✓'));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reviewed_file_prefix_tracks_state_and_clips_in_narrow_panes() {
|
|
||||||
let mut file = deletion_file();
|
|
||||||
file.path = "界.txt".into();
|
|
||||||
file.status = FileStatus::Modified;
|
|
||||||
let files = [file];
|
|
||||||
let mut picker = FilePicker::from_paths(PathBuf::new(), [(0, files[0].path.clone())]);
|
|
||||||
picker.set_view(FileView::List);
|
|
||||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(12, 3)).unwrap();
|
|
||||||
let mut reviewed = BTreeSet::new();
|
|
||||||
for marked in [true, false, true] {
|
|
||||||
if marked {
|
|
||||||
reviewed.insert(0);
|
|
||||||
} else {
|
|
||||||
reviewed.remove(&0);
|
|
||||||
}
|
|
||||||
for width in 1..=8 {
|
|
||||||
terminal
|
|
||||||
.draw(|frame| {
|
|
||||||
frame.render_widget(
|
|
||||||
Paragraph::new("XXXXXXXXXXXX\nXXXXXXXXXXXX\nXXXXXXXXXXXX"),
|
|
||||||
frame.area(),
|
|
||||||
);
|
|
||||||
draw_file_list(
|
|
||||||
frame,
|
|
||||||
&picker,
|
|
||||||
Rect::new(0, 0, width, 3),
|
|
||||||
"",
|
|
||||||
&mut WheelPanels::default(),
|
|
||||||
Some((&files, &reviewed)),
|
|
||||||
);
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
let buffer = terminal.backend().buffer();
|
|
||||||
let prefix = if marked { "> ✓ M " } else { "> M " };
|
|
||||||
for (x, symbol) in prefix.chars().take(usize::from(width)).enumerate() {
|
|
||||||
assert_eq!(buffer[(x as u16, 1)].symbol(), symbol.to_string());
|
|
||||||
}
|
|
||||||
for x in width..12 {
|
|
||||||
assert_eq!(buffer[(x, 1)].symbol(), "X");
|
|
||||||
}
|
|
||||||
if width >= 3 && marked {
|
|
||||||
assert_eq!(buffer[(2, 1)].fg, Color::Green);
|
|
||||||
}
|
|
||||||
if width >= 5 {
|
|
||||||
assert_eq!(buffer[(4, 1)].fg, Color::Yellow);
|
|
||||||
}
|
|
||||||
if width >= 8 {
|
|
||||||
assert_eq!(buffer[(6, 1)].symbol(), "界");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normal_file_markers_keep_clean_rows_and_match_diff_colors_in_every_view() {
|
fn normal_file_markers_keep_clean_rows_and_match_diff_colors_in_every_view() {
|
||||||
use crate::repository::WorkingFileChange;
|
use crate::repository::WorkingFileChange;
|
||||||
|
|||||||
Reference in New Issue
Block a user