feat: mark fully checked collapsed diff folders
This commit is contained in:
@@ -8,7 +8,7 @@
|
|||||||
- [x] diff view: show the function/class context that a change hunk belongs to.
|
- [x] diff view: show the function/class context that a change hunk belongs to.
|
||||||
- inline scope labels with sticky continuation, chosen before implementation
|
- inline scope labels with sticky continuation, chosen before implementation
|
||||||
- each side uses its own syntax tree; scope changes within a hunk get separate labels
|
- each side uses its own syntax tree; scope changes within a hunk get separate labels
|
||||||
- [ ] diff view: annotate folder with checkmark if all items under given folder has been checked AND the folder is collapsed.
|
- [x] diff view: annotate folder with checkmark if all items under given folder has been checked AND the folder is collapsed.
|
||||||
- [ ] diff view: automatically detect changes and update the view when relevant bookmarks move change ids (regards bookmark-based diff view; i.e., user selected at least one bookmark in the before/after picker)
|
- [ ] diff view: automatically detect changes and update the view when relevant bookmarks move change ids (regards bookmark-based diff view; i.e., user selected at least one bookmark in the before/after picker)
|
||||||
- [ ] diff view: github integration (code review) - add comments on a pr, add review comments at specific lines of files, request changes, approve pr, etc.
|
- [ ] diff view: github integration (code review) - add comments on a pr, add review comments at specific lines of files, request changes, approve pr, etc.
|
||||||
- [ ] diff view: local review comments (same features as for github reviews) that allows review of local changes to deliver specific reviews to ai agents used for coding.
|
- [ ] diff view: local review comments (same features as for github reviews) that allows review of local changes to deliver specific reviews to ai agents used for coding.
|
||||||
|
|||||||
+3
-1
@@ -432,7 +432,9 @@ 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.
|
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.
|
||||||
|
|
||||||
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 left of A/M/D/R, separated by one blank column**, in Tree and List views. The modification indicator keeps its existing indentation; the review marker uses the preceding gutter space instead of adding padding. Diff rows use the selection background and cursor rather than a `>` prefix. 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 **`✓` immediately left of A/M/D/R, separated by one blank column**, in Tree and List views. The modification indicator keeps its existing indentation; the review marker uses the preceding gutter space instead of adding padding. Diff rows use the selection background and cursor rather than a `>` prefix. Directory review marks cannot be toggled directly, and ordinary filesystem pickers have no review marks. 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.
|
||||||
|
|
||||||
|
Collapsed folders show a green **`✓`** when every changed file beneath them is reviewed, including nested files and descendants hidden by filtering. Expanded folders stay unmarked. Folder marks update automatically when a descendant's review mark or the changed-file catalog changes.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|||||||
@@ -370,6 +370,20 @@ impl FilePicker {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Full catalog descendants, independent of filtering and folder expansion.
|
||||||
|
pub(crate) fn descendant_catalog_indices<'a>(
|
||||||
|
&'a self,
|
||||||
|
path: &'a Path,
|
||||||
|
) -> impl Iterator<Item = usize> + 'a {
|
||||||
|
let start = self
|
||||||
|
.nodes
|
||||||
|
.partition_point(|node| node.path.as_path() <= path);
|
||||||
|
self.nodes[start..]
|
||||||
|
.iter()
|
||||||
|
.take_while(move |node| node.path.starts_with(path))
|
||||||
|
.filter_map(|node| node.catalog_index)
|
||||||
|
}
|
||||||
|
|
||||||
fn selected_node(&self) -> Option<usize> {
|
fn selected_node(&self) -> Option<usize> {
|
||||||
self.rows.get(self.selected).map(|row| row.node)
|
self.rows.get(self.selected).map(|row| row.node)
|
||||||
}
|
}
|
||||||
|
|||||||
+103
-3
@@ -5692,9 +5692,21 @@ fn draw_file_list(
|
|||||||
.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 mut spans = Vec::with_capacity(8);
|
let mut spans = Vec::with_capacity(8);
|
||||||
let reviewed = changed.is_some()
|
let reviewed = changed_files.is_some_and(|(_, reviewed)| {
|
||||||
&& entry.catalog_index.is_some_and(|index| {
|
if entry.is_dir {
|
||||||
changed_files.is_some_and(|(_, reviewed)| reviewed.contains(&index))
|
if entry.expanded {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let mut descendants =
|
||||||
|
picker.descendant_catalog_indices(entry.path).peekable();
|
||||||
|
descendants.peek().is_some()
|
||||||
|
&& descendants.all(|index| reviewed.contains(&index))
|
||||||
|
} else {
|
||||||
|
changed.is_some()
|
||||||
|
&& entry
|
||||||
|
.catalog_index
|
||||||
|
.is_some_and(|index| reviewed.contains(&index))
|
||||||
|
}
|
||||||
});
|
});
|
||||||
if changed_files.is_none() {
|
if changed_files.is_none() {
|
||||||
spans.push(Span::raw(if selected { ">" } else { " " }));
|
spans.push(Span::raw(if selected { ">" } else { " " }));
|
||||||
@@ -9541,6 +9553,94 @@ mod tests {
|
|||||||
assert!(!screen_text(&terminal).contains('✓'));
|
assert!(!screen_text(&terminal).contains('✓'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn collapsed_diff_folders_reflect_all_catalog_descendants() {
|
||||||
|
let files: Vec<_> = [
|
||||||
|
"src/top.rs",
|
||||||
|
"src/deep/leaf.rs",
|
||||||
|
"src/deep/other.rs",
|
||||||
|
"srcish/unreviewed.rs",
|
||||||
|
"src",
|
||||||
|
"compact/a/b/only.rs",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let mut file = deletion_file();
|
||||||
|
file.path = path.into();
|
||||||
|
file
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let paths = || {
|
||||||
|
files
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, file)| (index, file.path.clone()))
|
||||||
|
};
|
||||||
|
let mut picker = FilePicker::from_paths(PathBuf::new(), paths());
|
||||||
|
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(100, 18)).unwrap();
|
||||||
|
let mut check = |picker: &FilePicker, reviewed: &BTreeSet<usize>, label: &str, marked| {
|
||||||
|
terminal
|
||||||
|
.draw(|frame| {
|
||||||
|
draw_file_list(
|
||||||
|
frame,
|
||||||
|
picker,
|
||||||
|
frame.area(),
|
||||||
|
"",
|
||||||
|
&mut WheelPanels::default(),
|
||||||
|
Some((&files, reviewed)),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let text = screen_text(&terminal);
|
||||||
|
let row = text.lines().find(|row| row.contains(label)).unwrap();
|
||||||
|
assert_eq!(row.contains('✓'), marked, "{row}");
|
||||||
|
if marked {
|
||||||
|
let buffer = terminal.backend().buffer();
|
||||||
|
let y = text.lines().position(|row| row.contains(label)).unwrap() as u16;
|
||||||
|
let x = (0..100).find(|&x| buffer[(x, y)].symbol() == "✓").unwrap();
|
||||||
|
assert_eq!(buffer[(x, y)].fg, Color::Green);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut reviewed = BTreeSet::from([0, 1]);
|
||||||
|
check(&picker, &reviewed, "src/", false);
|
||||||
|
reviewed.insert(2);
|
||||||
|
check(&picker, &reviewed, "src/", true);
|
||||||
|
check(&picker, &reviewed, "srcish/", false);
|
||||||
|
check(&picker, &reviewed, "compact/a/b/", false);
|
||||||
|
reviewed.insert(5);
|
||||||
|
check(&picker, &reviewed, "compact/a/b/", true);
|
||||||
|
|
||||||
|
let selected = picker
|
||||||
|
.entries()
|
||||||
|
.position(|row| row.is_dir && row.path == std::path::Path::new("src"))
|
||||||
|
.unwrap();
|
||||||
|
picker.selected = selected;
|
||||||
|
picker.right();
|
||||||
|
check(&picker, &reviewed, "src/", false);
|
||||||
|
check(&picker, &reviewed, "deep/", true);
|
||||||
|
|
||||||
|
reviewed.remove(&2);
|
||||||
|
picker.query = "leaf.rs".into();
|
||||||
|
picker.refilter();
|
||||||
|
let selected = picker
|
||||||
|
.entries()
|
||||||
|
.position(|row| row.is_dir && row.path == std::path::Path::new("src/deep"))
|
||||||
|
.unwrap();
|
||||||
|
picker.selected = selected;
|
||||||
|
picker.accept();
|
||||||
|
check(&picker, &reviewed, "deep/", false);
|
||||||
|
reviewed.insert(2);
|
||||||
|
check(&picker, &reviewed, "deep/", true);
|
||||||
|
reviewed.remove(&2);
|
||||||
|
check(&picker, &reviewed, "deep/", false);
|
||||||
|
|
||||||
|
// A reverted file no longer in the picker must not block its folder's mark.
|
||||||
|
picker.replace_paths(paths().filter(|(index, _)| *index != 2));
|
||||||
|
check(&picker, &reviewed, "deep/", true);
|
||||||
|
picker.accept();
|
||||||
|
check(&picker, &reviewed, "deep/", false);
|
||||||
|
}
|
||||||
|
|
||||||
#[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