feat: support comments in ordinary source buffers
This commit is contained in:
+107
-78
@@ -2460,6 +2460,7 @@ fn column_view_suppresses_whole_file_emphasis_without_losing_change_markers() {
|
||||
|
||||
#[test]
|
||||
fn working_refresh_preserves_noops_errors_and_view_state_but_invalidates_old_identity() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut state = live_state(vec!["old".into()], vec!["new".into()], false);
|
||||
state.loading = false;
|
||||
state.pending = None;
|
||||
@@ -2554,7 +2555,7 @@ fn working_refresh_preserves_noops_errors_and_view_state_but_invalidates_old_ide
|
||||
.unwrap()
|
||||
.editable_paths
|
||||
.insert("file".into());
|
||||
state.refresh_after(&["dirty".into()], 1);
|
||||
state.refresh_after(&["dirty".into()], 1, &mut review);
|
||||
assert_eq!(state.comparison.as_ref().unwrap().files[1].after, ["dirty"]);
|
||||
state.queue_checkout_refresh(false);
|
||||
state.take_request();
|
||||
@@ -2575,6 +2576,7 @@ fn working_refresh_preserves_noops_errors_and_view_state_but_invalidates_old_ide
|
||||
|
||||
#[test]
|
||||
fn reviewed_files_use_highlighted_catalog_indices_not_sorted_or_filtered_rows() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut state = DiffState::new();
|
||||
let mut changes = comparison();
|
||||
changes.files[0].path = "nested/z.txt".into();
|
||||
@@ -2591,9 +2593,9 @@ fn reviewed_files_use_highlighted_catalog_indices_not_sorted_or_filtered_rows()
|
||||
}
|
||||
files.reveal_path(Path::new("nested/a.txt"));
|
||||
assert_eq!(state.file, 0);
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([1]));
|
||||
assert_eq!(state.toggle_reviewed(), Ok(false));
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(false));
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
}
|
||||
let files = state.files.as_mut().unwrap();
|
||||
@@ -2602,7 +2604,7 @@ fn reviewed_files_use_highlighted_catalog_indices_not_sorted_or_filtered_rows()
|
||||
let selected = files.entries().position(|entry| entry.is_dir).unwrap();
|
||||
files.selected = selected;
|
||||
assert_eq!(
|
||||
state.toggle_reviewed(),
|
||||
state.toggle_reviewed(&mut review),
|
||||
Err("Select a changed file, not a directory")
|
||||
);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
@@ -2611,32 +2613,42 @@ fn reviewed_files_use_highlighted_catalog_indices_not_sorted_or_filtered_rows()
|
||||
for ch in "missing".chars() {
|
||||
files.handle_filter_input(&Input::Char(ch));
|
||||
}
|
||||
assert_eq!(state.toggle_reviewed(), Err("No changed file is selected"));
|
||||
assert_eq!(
|
||||
state.toggle_reviewed(&mut review),
|
||||
Err("No changed file is selected")
|
||||
);
|
||||
state.files = Some(FilePicker::from_paths(
|
||||
PathBuf::new(),
|
||||
[(0, PathBuf::from("ordinary.txt"))],
|
||||
));
|
||||
assert_eq!(state.toggle_reviewed(), Err("No changed file is selected"));
|
||||
assert_eq!(
|
||||
state.toggle_reviewed(&mut review),
|
||||
Err("No changed file is selected")
|
||||
);
|
||||
state.focus = Focus::Before;
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
assert!(state.select_file_index(2));
|
||||
state.focus = Focus::Before;
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0, 2]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reviewed_files_survive_lazy_details_and_views_but_reset_on_comparison() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut state = DiffState::new();
|
||||
assert_eq!(state.toggle_reviewed(), Err("No comparison is open"));
|
||||
assert_eq!(
|
||||
state.toggle_reviewed(&mut review),
|
||||
Err("No comparison is open")
|
||||
);
|
||||
let mut changes = comparison();
|
||||
changes.files[0].loaded = false;
|
||||
changes.files[0].before.clear();
|
||||
changes.files[0].after.clear();
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(1, Ok(Outcome::Comparison(changes)));
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
let file = comparison().files.remove(0);
|
||||
state.apply_result(
|
||||
@@ -2667,10 +2679,13 @@ fn reviewed_files_survive_lazy_details_and_views_but_reset_on_comparison() {
|
||||
));
|
||||
state.toggle_comparison_mode().unwrap();
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Err("No comparison is open"));
|
||||
assert_eq!(
|
||||
state.toggle_reviewed(&mut review),
|
||||
Err("No comparison is open")
|
||||
);
|
||||
state.apply_result(3, Ok(Outcome::Comparison(comparison())));
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(4, Ok(Outcome::Comparison(comparison())));
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
@@ -2678,17 +2693,18 @@ fn reviewed_files_survive_lazy_details_and_views_but_reset_on_comparison() {
|
||||
|
||||
#[test]
|
||||
fn restored_review_marks_use_file_identities_and_rename_paths_not_catalog_order() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut state = DiffState::new();
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(1, Ok(Outcome::Comparison(comparison())));
|
||||
state.comparison.as_mut().unwrap().files[0].review_revisions =
|
||||
Some(["file:before".into(), "file:after".into()]);
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
state.comparison.as_mut().unwrap().files.swap(0, 1);
|
||||
state.restore_reviewed_files();
|
||||
state.restore_reviewed_files(&review);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([1]));
|
||||
state.comparison.as_mut().unwrap().files[1].old_path = Some("different.txt".into());
|
||||
state.restore_reviewed_files();
|
||||
state.restore_reviewed_files(&review);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
state.comparison.as_mut().unwrap().files[1].old_path = Some("old.txt".into());
|
||||
for side in [0, 1] {
|
||||
@@ -2698,7 +2714,7 @@ fn restored_review_marks_use_file_identities_and_rename_paths_not_catalog_order(
|
||||
.unwrap();
|
||||
let original = identities[side].clone();
|
||||
identities[side].push_str("-different");
|
||||
state.restore_reviewed_files();
|
||||
state.restore_reviewed_files(&review);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
state.comparison.as_mut().unwrap().files[1]
|
||||
.review_revisions
|
||||
@@ -2706,12 +2722,13 @@ fn restored_review_marks_use_file_identities_and_rename_paths_not_catalog_order(
|
||||
.unwrap()[side] = original;
|
||||
}
|
||||
state.comparison.as_mut().unwrap().files[1].loaded = false;
|
||||
state.restore_reviewed_files();
|
||||
state.restore_reviewed_files(&review);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([1]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unchanged_file_review_survives_other_files_changing() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
use crate::repository::Repository;
|
||||
use std::sync::{Arc, atomic::AtomicBool};
|
||||
let root = std::env::temp_dir().join(format!("vamp-review-files-{}", std::process::id()));
|
||||
@@ -2759,13 +2776,13 @@ fn unchanged_file_review_survives_other_files_changing() {
|
||||
let original_revisions = changes.revisions.clone();
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(1, Ok(Outcome::Comparison(changes)));
|
||||
state.toggle_reviewed().unwrap();
|
||||
state.toggle_reviewed(&mut review).unwrap();
|
||||
state.file = 1;
|
||||
state.focus = Focus::Before;
|
||||
state.toggle_reviewed().unwrap();
|
||||
state.toggle_reviewed(&mut review).unwrap();
|
||||
let mut store = crate::review::Store::open(&root.join("reviews")).unwrap();
|
||||
store.save(&mut state.local_review.document).unwrap();
|
||||
let id = state.local_review.document.storage_id.unwrap();
|
||||
store.save(&mut review.document).unwrap();
|
||||
let id = review.document.storage_id.unwrap();
|
||||
std::fs::write(root.join("b.txt"), "edited again\n").unwrap();
|
||||
let changes = repository
|
||||
.compare(2, &before, &after, ComparisonMode::Snapshots, &[])
|
||||
@@ -2773,8 +2790,8 @@ fn unchanged_file_review_survives_other_files_changing() {
|
||||
assert_ne!(changes.revisions, original_revisions);
|
||||
state.request_kind = Some(RequestKind::Comparison);
|
||||
state.apply_result(2, Ok(Outcome::Comparison(changes)));
|
||||
state.local_review.document = store.load(&root, id).unwrap();
|
||||
state.restore_reviewed_files();
|
||||
review.document = store.load(&root, id).unwrap();
|
||||
state.restore_reviewed_files(&review);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
let detail = repository.load_file(2, 0).unwrap();
|
||||
state.apply_result(
|
||||
@@ -2785,33 +2802,36 @@ fn unchanged_file_review_survives_other_files_changing() {
|
||||
file: detail,
|
||||
}),
|
||||
);
|
||||
state.restore_reviewed_files();
|
||||
state.restore_reviewed_files(&review);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restored_live_review_marks_require_exact_text_and_final_newline() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut reopened_review = crate::review::ReviewState::default();
|
||||
let mut state = live_state(vec!["base".into()], vec!["changed".into()], false);
|
||||
state.refresh_after(&["unsaved".into(), String::new()], 1);
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
let document = state.local_review.document.clone();
|
||||
state.refresh_after(&["unsaved".into(), String::new()], 1, &mut review);
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
let document = review.document.clone();
|
||||
let mut reopened = live_state(vec!["base".into()], vec!["changed".into()], false);
|
||||
reopened.local_review.document = document.clone();
|
||||
reopened.restore_reviewed_files();
|
||||
reopened_review.document = document.clone();
|
||||
reopened.restore_reviewed_files(&reopened_review);
|
||||
assert!(reopened.reviewed_files.is_empty());
|
||||
reopened.refresh_after(&["unsaved".into(), String::new()], 1);
|
||||
reopened.refresh_after(&["unsaved".into(), String::new()], 1, &mut reopened_review);
|
||||
assert_eq!(reopened.reviewed_files, BTreeSet::from([0]));
|
||||
reopened.refresh_after(&["unsaved".into()], 2);
|
||||
reopened.refresh_after(&["unsaved".into()], 2, &mut reopened_review);
|
||||
assert!(reopened.reviewed_files.is_empty());
|
||||
assert!(reopened.local_review.document.reviewed_files.is_empty());
|
||||
reopened.local_review.document = document;
|
||||
reopened.restore_reviewed_files();
|
||||
assert!(reopened_review.document.reviewed_files.is_empty());
|
||||
reopened_review.document = document;
|
||||
reopened.restore_reviewed_files(&reopened_review);
|
||||
assert!(reopened.reviewed_files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn working_review_marks_clear_only_for_changed_bytes_or_eof() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
for (after, eof, lines) in [
|
||||
(vec!["changed".into()], false, vec!["changed".into()]),
|
||||
(
|
||||
@@ -2822,24 +2842,24 @@ fn working_review_marks_clear_only_for_changed_bytes_or_eof() {
|
||||
(Vec::new(), false, vec![String::new()]),
|
||||
] {
|
||||
let mut state = live_state(vec!["base".into()], after, eof);
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&lines, 1);
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
state.refresh_after(&lines, 1, &mut review);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
state.refresh_after(&lines, 2);
|
||||
state.refresh_after(&lines, 2, &mut review);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
state.refresh_after(&["edited".into()], 3);
|
||||
state.refresh_after(&["edited".into()], 3, &mut review);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&["edited".into(), String::new()], 4);
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
state.refresh_after(&["edited".into(), String::new()], 4, &mut review);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&["edited".into()], 5);
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
state.refresh_after(&["edited".into()], 5, &mut review);
|
||||
assert!(state.reviewed_files.is_empty());
|
||||
}
|
||||
let mut state = live_state(vec!["base".into()], vec!["changed".into()], false);
|
||||
state.comparison.as_mut().unwrap().files[0].loaded = false;
|
||||
assert_eq!(state.toggle_reviewed(), Ok(true));
|
||||
state.refresh_after(&["edited".into()], 1);
|
||||
assert_eq!(state.toggle_reviewed(&mut review), Ok(true));
|
||||
state.refresh_after(&["edited".into()], 1, &mut review);
|
||||
assert_eq!(state.reviewed_files, BTreeSet::from([0]));
|
||||
}
|
||||
|
||||
@@ -2901,19 +2921,20 @@ fn unified_groups_changed_runs_and_roundtrips_side_specific_source_rows() {
|
||||
|
||||
#[test]
|
||||
fn unified_live_reflow_preserves_removed_top_and_expanded_source_excerpts() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let base: Vec<_> = (0..40).map(|row| format!("line {row}")).collect();
|
||||
let mut lines = base.clone();
|
||||
lines[20] = "new line".into();
|
||||
let mut state = live_state(base, lines.clone(), false);
|
||||
state.set_context_options(1, 3);
|
||||
state.refresh_after(&lines, 1);
|
||||
state.refresh_after(&lines, 1, &mut review);
|
||||
state.set_view_mode(ViewMode::Unified);
|
||||
state.top = state.source_display_row(Focus::Before, 20).unwrap();
|
||||
state.expand_source_excerpt(Focus::After, false, 20);
|
||||
assert_eq!(state.display_source_row(Focus::Before, state.top), Some(20));
|
||||
assert!(state.source_display_row(Focus::After, 16).is_some());
|
||||
lines.insert(0, "inserted".into());
|
||||
state.refresh_after(&lines, 2);
|
||||
state.refresh_after(&lines, 2, &mut review);
|
||||
assert_eq!(state.display_focus(state.top), Some(Focus::Before));
|
||||
assert_eq!(state.display_source_row(Focus::Before, state.top), Some(20));
|
||||
assert!(state.source_display_row(Focus::After, 17).is_some());
|
||||
@@ -2932,6 +2953,7 @@ fn unified_live_reflow_preserves_removed_top_and_expanded_source_excerpts() {
|
||||
|
||||
#[test]
|
||||
fn character_ranges_follow_insertions_deletions_unicode_and_live_edits() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
for (before, after, expected) in [
|
||||
("ab", "a界b", vec![[1..1, 1..2]]),
|
||||
("a界b", "ab", vec![[1..2, 1..1]]),
|
||||
@@ -2941,17 +2963,18 @@ fn character_ranges_follow_insertions_deletions_unicode_and_live_edits() {
|
||||
assert_eq!(state.rows[0].changes, expected);
|
||||
}
|
||||
let mut state = live_state(vec!["abc".into()], vec!["aXbc".into()], false);
|
||||
state.refresh_after(&["aXYbc".into()], 1);
|
||||
state.refresh_after(&["aXYbc".into()], 1, &mut review);
|
||||
assert_eq!(state.rows[0].changes, vec![[1..1, 1..3]]);
|
||||
state.refresh_after(&["abYZc".into()], 2);
|
||||
state.refresh_after(&["abYZc".into()], 2, &mut review);
|
||||
assert_eq!(state.rows[0].changes, vec![[2..2, 2..4]]);
|
||||
state.refresh_after(&["abc".into()], 3);
|
||||
state.refresh_after(&["abc".into()], 3, &mut review);
|
||||
assert_eq!(state.rows[0].kind, RowKind::Equal);
|
||||
assert!(state.rows[0].changes.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn character_highlights_merge_incidental_matches_but_preserve_meaningful_gaps() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut cases = vec![
|
||||
("a..c".into(), "b..d".into(), vec![[0..4, 0..4]]),
|
||||
("base".into(), "edited".into(), vec![[0..4, 0..6]]),
|
||||
@@ -2995,14 +3018,15 @@ fn character_highlights_merge_incidental_matches_but_preserve_meaningful_gaps()
|
||||
for (before, after, expected) in cases {
|
||||
let mut state = live_state(vec![before], vec![after.clone()], false);
|
||||
assert_eq!(state.rows[0].changes, expected, "{after:?}");
|
||||
state.refresh_after(&[format!("{after}!")], 1);
|
||||
state.refresh_after(std::slice::from_ref(&after), 2);
|
||||
state.refresh_after(&[format!("{after}!")], 1, &mut review);
|
||||
state.refresh_after(std::slice::from_ref(&after), 2, &mut review);
|
||||
assert_eq!(state.rows[0].changes, expected, "live edit: {after:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reverting_loaded_file_keeps_unloaded_changes_selectable() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut state = live_state(vec!["base".into()], vec!["changed".into()], false);
|
||||
state.comparison_id = Some(17);
|
||||
state.pending = None;
|
||||
@@ -3020,7 +3044,7 @@ fn reverting_loaded_file_keeps_unloaded_changes_selectable() {
|
||||
hunks: vec![],
|
||||
notice: None,
|
||||
});
|
||||
state.refresh_after(&["base".into()], 1);
|
||||
state.refresh_after(&["base".into()], 1, &mut review);
|
||||
assert_eq!(
|
||||
state
|
||||
.files
|
||||
@@ -3043,36 +3067,37 @@ fn reverting_loaded_file_keeps_unloaded_changes_selectable() {
|
||||
|
||||
#[test]
|
||||
fn live_width_cache_scans_only_changed_lines_and_shrinks_after_deletion() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let base = vec!["original".into(), "medium".into(), "short".into()];
|
||||
let mut lines = vec!["界\tz".into(), "medium".into(), "short".into()];
|
||||
let mut state = live_state(base, lines.clone(), false);
|
||||
state.refresh_after(&lines, 1);
|
||||
state.refresh_after(&lines, 1, &mut review);
|
||||
assert_eq!(state.after_widths, [5, 6, 5]);
|
||||
lines[0] = "界界\t🙂".into();
|
||||
WIDTH_SCANS.set(0);
|
||||
state.refresh_after(&lines, 2);
|
||||
state.refresh_after(&lines, 2, &mut review);
|
||||
assert_eq!(WIDTH_SCANS.get(), 1);
|
||||
assert_eq!(state.max_after, 10);
|
||||
state.left_after = 9;
|
||||
lines[0] = "界".into();
|
||||
WIDTH_SCANS.set(0);
|
||||
state.refresh_after(&lines, 3);
|
||||
state.refresh_after(&lines, 3, &mut review);
|
||||
assert_eq!(WIDTH_SCANS.get(), 1);
|
||||
assert_eq!(state.max_after, 6);
|
||||
assert_eq!(state.left_after, 6);
|
||||
lines.remove(1);
|
||||
WIDTH_SCANS.set(0);
|
||||
state.refresh_after(&lines, 4);
|
||||
state.refresh_after(&lines, 4, &mut review);
|
||||
assert_eq!(WIDTH_SCANS.get(), 0);
|
||||
assert_eq!(state.after_widths, [2, 5]);
|
||||
assert_eq!(state.max_after, 5);
|
||||
lines.insert(1, "巨大\t🙂".into());
|
||||
WIDTH_SCANS.set(0);
|
||||
state.refresh_after(&lines, 5);
|
||||
state.refresh_after(&lines, 5, &mut review);
|
||||
assert_eq!(WIDTH_SCANS.get(), 1);
|
||||
assert_eq!(state.after_widths, [2, 10, 5]);
|
||||
lines = vec!["".into()];
|
||||
state.refresh_after(&lines, 6);
|
||||
state.refresh_after(&lines, 6, &mut review);
|
||||
assert_eq!(state.after_widths, [0]);
|
||||
assert_eq!(state.max_after, 0);
|
||||
assert_eq!(state.left_after, 0);
|
||||
@@ -3080,6 +3105,7 @@ fn live_width_cache_scans_only_changed_lines_and_shrinks_after_deletion() {
|
||||
|
||||
#[test]
|
||||
fn stable_line_edits_keep_fresh_hunks_expansion_and_scroll_anchor_until_revert() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let base = (0..100)
|
||||
.map(|row| format!("line {row}"))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -3088,7 +3114,7 @@ fn stable_line_edits_keep_fresh_hunks_expansion_and_scroll_anchor_until_revert()
|
||||
let mut state = live_state(base.clone(), lines.clone(), true);
|
||||
state.set_context_options(2, 5);
|
||||
lines.push(String::new());
|
||||
state.refresh_after(&lines, 1);
|
||||
state.refresh_after(&lines, 1, &mut review);
|
||||
state.top = state.after_display_row(30).unwrap();
|
||||
state.handle(Input::Char('['));
|
||||
let display = state.display_rows.clone();
|
||||
@@ -3102,7 +3128,7 @@ fn stable_line_edits_keep_fresh_hunks_expansion_and_scroll_anchor_until_revert()
|
||||
changed_line_range(&file.after, &lines)
|
||||
));
|
||||
WIDTH_SCANS.set(0);
|
||||
state.refresh_after(&lines, 2);
|
||||
state.refresh_after(&lines, 2, &mut review);
|
||||
assert_eq!(WIDTH_SCANS.get(), 1);
|
||||
assert_eq!(state.display_rows, display);
|
||||
assert_eq!(state.excerpt_ranges, excerpts);
|
||||
@@ -3131,14 +3157,14 @@ fn stable_line_edits_keep_fresh_hunks_expansion_and_scroll_anchor_until_revert()
|
||||
&lines,
|
||||
changed_line_range(&file.after, &lines)
|
||||
));
|
||||
state.refresh_after(&lines, 3);
|
||||
state.refresh_after(&lines, 3, &mut review);
|
||||
assert!(state.comparison.as_ref().unwrap().files[0].hunks.is_empty());
|
||||
assert!(state.rows.iter().all(|row| row.kind == RowKind::Equal));
|
||||
assert!(state.after_display_row(23).is_some());
|
||||
assert_eq!(state.top, state.after_display_row(30).unwrap());
|
||||
assert_eq!(state.files.as_ref().unwrap().entries().len(), 0);
|
||||
lines[50] = "changed again".into();
|
||||
state.refresh_after(&lines, 4);
|
||||
state.refresh_after(&lines, 4, &mut review);
|
||||
assert_eq!(state.files.as_ref().unwrap().entries().len(), 1);
|
||||
assert!(state.after_display_row(23).is_some());
|
||||
assert!(state.after_display_row(50).is_some());
|
||||
@@ -3146,6 +3172,7 @@ fn stable_line_edits_keep_fresh_hunks_expansion_and_scroll_anchor_until_revert()
|
||||
|
||||
#[test]
|
||||
fn live_reflow_keeps_separate_hunks_and_expanded_context_after_revert() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let base = (0..100)
|
||||
.map(|row| format!("line {row}"))
|
||||
.collect::<Vec<_>>();
|
||||
@@ -3155,7 +3182,7 @@ fn live_reflow_keeps_separate_hunks_and_expanded_context_after_revert() {
|
||||
let mut state = live_state(base.clone(), changed.clone(), true);
|
||||
state.set_context_options(2, 5);
|
||||
changed.push(String::new());
|
||||
state.refresh_after(&changed, 1);
|
||||
state.refresh_after(&changed, 1, &mut review);
|
||||
assert_eq!(state.comparison.as_ref().unwrap().files[0].hunks.len(), 2);
|
||||
assert!(
|
||||
state
|
||||
@@ -3167,7 +3194,7 @@ fn live_reflow_keeps_separate_hunks_and_expanded_context_after_revert() {
|
||||
state.handle(Input::Char('['));
|
||||
assert!(state.after_display_row(23).is_some());
|
||||
changed.insert(10, "inserted outside excerpts".into());
|
||||
state.refresh_after(&changed, 2);
|
||||
state.refresh_after(&changed, 2, &mut review);
|
||||
assert!(state.after_display_row(24).is_some());
|
||||
assert_eq!(state.comparison.as_ref().unwrap().files[0].hunks.len(), 3);
|
||||
assert!(
|
||||
@@ -3178,7 +3205,7 @@ fn live_reflow_keeps_separate_hunks_and_expanded_context_after_revert() {
|
||||
);
|
||||
let mut reverted = base;
|
||||
reverted.push(String::new());
|
||||
state.refresh_after(&reverted, 3);
|
||||
state.refresh_after(&reverted, 3, &mut review);
|
||||
assert!(state.comparison.as_ref().unwrap().files[0].hunks.is_empty());
|
||||
assert!(state.rows.iter().all(|row| row.kind == RowKind::Equal));
|
||||
assert!(state.after_display_row(23).is_some());
|
||||
@@ -3186,21 +3213,22 @@ fn live_reflow_keeps_separate_hunks_and_expanded_context_after_revert() {
|
||||
assert_eq!(state.files.as_ref().unwrap().entries().len(), 0);
|
||||
assert_eq!(state.file, 0);
|
||||
reverted[50] = "changed again".into();
|
||||
state.refresh_after(&reverted, 4);
|
||||
state.refresh_after(&reverted, 4, &mut review);
|
||||
assert_eq!(state.files.as_ref().unwrap().entries().len(), 1);
|
||||
assert!(state.after_display_row(50).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn live_empty_and_eof_rows_match_buffer_without_false_eof_highlights() {
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
let mut state = live_state(vec!["same".into()], vec!["changed".into()], true);
|
||||
state.refresh_after(&["same".into(), "".into()], 1);
|
||||
state.refresh_after(&["same".into(), "".into()], 1, &mut review);
|
||||
assert!(state.comparison.as_ref().unwrap().files[0].hunks.is_empty());
|
||||
assert_eq!(state.rows.len(), 2);
|
||||
assert!(state.rows.iter().all(|row| row.kind == RowKind::Equal));
|
||||
assert!(state.after_display_row(0).is_some());
|
||||
assert!(state.after_display_row(1).is_none());
|
||||
state.refresh_after(&["same".into()], 2);
|
||||
state.refresh_after(&["same".into()], 2, &mut review);
|
||||
assert_eq!(state.rows[0].kind, RowKind::Changed);
|
||||
assert!(
|
||||
state.comparison.as_ref().unwrap().files[0]
|
||||
@@ -3209,18 +3237,18 @@ fn live_empty_and_eof_rows_match_buffer_without_false_eof_highlights() {
|
||||
.unwrap()
|
||||
.contains("After missing")
|
||||
);
|
||||
state.refresh_after(&["".into()], 3);
|
||||
state.refresh_after(&["".into()], 3, &mut review);
|
||||
assert!(state.after_display_row(0).is_some());
|
||||
assert_eq!(state.comparison.as_ref().unwrap().files[0].after, [""]);
|
||||
let mut empty = live_state(vec![], vec!["text".into()], false);
|
||||
empty.refresh_after(&["".into()], 1);
|
||||
empty.refresh_after(&["".into()], 1, &mut review);
|
||||
assert!(empty.comparison.as_ref().unwrap().files[0].hunks.is_empty());
|
||||
assert_eq!(empty.rows.len(), 1);
|
||||
assert_eq!(empty.rows[0].kind, RowKind::Equal);
|
||||
assert_eq!(empty.after_display_row(0), Some(0));
|
||||
empty.after.as_mut().unwrap().target = RevisionTarget::Commit("historic".into());
|
||||
empty.comparison.as_mut().unwrap().editable_paths.clear();
|
||||
empty.refresh_after(&["must not apply".into()], 2);
|
||||
empty.refresh_after(&["must not apply".into()], 2, &mut review);
|
||||
assert_eq!(empty.comparison.as_ref().unwrap().files[0].after, [""]);
|
||||
empty.handle(Input::Escape);
|
||||
assert_eq!(empty.phase, Phase::View);
|
||||
@@ -3372,6 +3400,7 @@ fn mode_action_keeps_filter_input_and_rejects_superseded_comparisons() {
|
||||
|
||||
#[test]
|
||||
fn review_anchors_match_frozen_context_and_exact_live_eof_without_allocating() {
|
||||
let review = crate::review::ReviewState::default();
|
||||
use crate::review::{Anchor, Side};
|
||||
let mut state = live_state(vec!["old".into()], vec!["new".into()], true);
|
||||
let anchor = Anchor {
|
||||
@@ -3382,15 +3411,15 @@ fn review_anchors_match_frozen_context_and_exact_live_eof_without_allocating() {
|
||||
end: crate::plugin::TextPosition { row: 1, column: 0 },
|
||||
content: "new\n".into(),
|
||||
};
|
||||
assert!(state.review_anchor_range(&anchor).is_some());
|
||||
assert!(state.review_anchor_range(&anchor, &review).is_some());
|
||||
state.live_files.insert(0);
|
||||
assert!(state.review_anchor_range(&anchor).is_none());
|
||||
assert!(state.review_anchor_range(&anchor, &review).is_none());
|
||||
state.comparison.as_mut().unwrap().files[0]
|
||||
.after
|
||||
.push(String::new());
|
||||
assert!(state.review_anchor_range(&anchor).is_some());
|
||||
assert!(state.review_anchor_range(&anchor, &review).is_some());
|
||||
state.comparison.as_mut().unwrap().files[0].after[0] = "changed".into();
|
||||
assert!(state.review_anchor_range(&anchor).is_none());
|
||||
assert!(state.review_anchor_range(&anchor, &review).is_none());
|
||||
state.comparison.as_mut().unwrap().files[0].old_path = Some("original".into());
|
||||
let before = Anchor {
|
||||
revision: "base".into(),
|
||||
@@ -3399,12 +3428,12 @@ fn review_anchors_match_frozen_context_and_exact_live_eof_without_allocating() {
|
||||
content: "old\n".into(),
|
||||
..anchor
|
||||
};
|
||||
assert!(state.review_anchor_range(&before).is_some());
|
||||
assert!(state.review_anchor_range(&before, &review).is_some());
|
||||
state.comparison.as_mut().unwrap().files[0].loaded = false;
|
||||
assert!(state.review_anchor_range(&before).is_none());
|
||||
assert!(state.review_anchor_range(&before, &review).is_none());
|
||||
state.comparison.as_mut().unwrap().files[0].loaded = true;
|
||||
state.comparison.as_mut().unwrap().revisions[0] = "other".into();
|
||||
assert!(state.review_anchor_range(&before).is_none());
|
||||
assert!(state.review_anchor_range(&before, &review).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+193
-160
@@ -3139,6 +3139,160 @@ fn historical_diff(editor: &mut Editor, root: &Path, before: Vec<String>, after:
|
||||
assert!(editor.diff_interacting());
|
||||
}
|
||||
|
||||
fn source_review_editor(directory: &Directory, text: &str) -> (Editor, PathBuf, PathBuf) {
|
||||
let root = directory.0.canonicalize().unwrap();
|
||||
assert!(
|
||||
std::process::Command::new("git")
|
||||
.args(["init", "-q", "--template="])
|
||||
.current_dir(&root)
|
||||
.status()
|
||||
.unwrap()
|
||||
.success()
|
||||
);
|
||||
let path = root.join("source.txt");
|
||||
fs::write(&path, text).unwrap();
|
||||
let mut editor = Editor::open(Some(path.clone()), vec!["text".into()]).unwrap();
|
||||
editor.set_clipboard_mode("separate").unwrap();
|
||||
editor.set_review_directory(root.join("reviews"));
|
||||
(editor, root, path)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_review_drafts_guard_source_pane_close_and_saved_panels_retarget() {
|
||||
let directory = Directory::new();
|
||||
let (mut editor, _, _) = source_review_editor(&directory, "source\n");
|
||||
let source = editor.pane_id;
|
||||
editor.split_pane(SplitAxis::Vertical);
|
||||
let other = editor.pane_id;
|
||||
editor.select_pane(source);
|
||||
editor.execute("comment");
|
||||
type_text(&mut editor, "iDraft");
|
||||
editor.handle(Input::Escape);
|
||||
editor.select_pane(source);
|
||||
editor.close_pane(false);
|
||||
assert!(editor.panes.contains_key(&source));
|
||||
editor.execute("review.save");
|
||||
editor.close_pane(false);
|
||||
assert!(!editor.panes.contains_key(&source));
|
||||
assert_eq!(editor.pane_id, other);
|
||||
editor.execute("review.edit 1");
|
||||
assert_eq!(editor.context().text, "Draft");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.pane_id, other);
|
||||
assert_eq!(editor.context().text, "source\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_comments_capture_unsaved_exact_unicode_and_reopen_without_a_diff() {
|
||||
let directory = Directory::new();
|
||||
let (mut editor, root, path) = source_review_editor(&directory, "disk\n");
|
||||
assert!(!root.join("reviews").exists());
|
||||
let source = "first\nprefix e\u{301}界 suffix\nlast\n";
|
||||
editor.apply(vec![Effect::Replace(source.into())]).unwrap();
|
||||
set_carets(&mut editor, &[(1, 7)], 0);
|
||||
type_text(&mut editor, "vl");
|
||||
editor.execute("comment");
|
||||
assert!(editor.review_panel().is_some());
|
||||
assert!(editor.workspace_diff().is_none());
|
||||
type_text(&mut editor, "iExact feedback");
|
||||
editor.handle(Input::Escape);
|
||||
editor.execute("w");
|
||||
let review = editor.local_review.document.clone();
|
||||
let anchor = review.comments[0].anchor.clone();
|
||||
assert_eq!(anchor.excerpt(), "e\u{301}界");
|
||||
assert_eq!(anchor.content, source);
|
||||
assert!(anchor.revision.starts_with("working:"));
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), "disk\n");
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.context().text, source);
|
||||
editor.execute("w");
|
||||
drop(editor);
|
||||
let mut reopened = Editor::open(Some(path), vec!["text".into()]).unwrap();
|
||||
reopened.set_review_directory(root.join("reviews"));
|
||||
assert!(reopened.workspace_diff().is_none());
|
||||
assert_eq!(reopened.local_review.document.storage_id, review.storage_id);
|
||||
reopened.execute("review.next");
|
||||
assert_eq!(reopened.context().text, "Exact feedback");
|
||||
assert!(reopened.review_panel_title().unwrap().contains("current"));
|
||||
reopened.handle(Input::Escape);
|
||||
assert_eq!(reopened.cursor(), Cursor { row: 1, column: 7 });
|
||||
assert_eq!(reopened.local_review.document.comments[0].anchor, anchor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_comment_dismissal_conflicts_tabs_and_stale_snapshots_preserve_text() {
|
||||
let directory = Directory::new();
|
||||
let (mut editor, root, path) = source_review_editor(&directory, "original line\n");
|
||||
editor.execute("comment");
|
||||
type_text(&mut editor, "iDiscarded");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Escape);
|
||||
assert!(editor.local_review.document.comments.is_empty());
|
||||
editor.execute("comment");
|
||||
type_text(&mut editor, "iSaved feedback");
|
||||
editor.handle(Input::Escape);
|
||||
editor.execute("w");
|
||||
editor.handle(Input::Escape);
|
||||
let anchor = editor.local_review.document.comments[0].anchor.clone();
|
||||
assert_eq!(anchor.excerpt(), "original line\n");
|
||||
editor.execute("tab.new");
|
||||
editor.execute("review.edit 1");
|
||||
assert_eq!(editor.context().text, "Saved feedback");
|
||||
editor.handle(Input::Escape);
|
||||
editor.execute("tab.previous");
|
||||
editor
|
||||
.apply(vec![Effect::Replace("entirely changed\n".into())])
|
||||
.unwrap();
|
||||
editor.execute("review.edit 1");
|
||||
assert!(editor.message().contains("stale"));
|
||||
assert!(
|
||||
editor
|
||||
.review_panel_title()
|
||||
.unwrap()
|
||||
.contains("original snapshot")
|
||||
);
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.context().text, "original line\n");
|
||||
assert!(editor.active.resolver_readonly);
|
||||
assert_eq!(editor.local_review.document.comments[0].anchor, anchor);
|
||||
assert_eq!(fs::read_to_string(&path).unwrap(), "original line\n");
|
||||
let mut concurrent = Editor::open(Some(path), vec!["text".into()]).unwrap();
|
||||
concurrent.set_review_directory(root.join("reviews"));
|
||||
concurrent.execute("review.verdict looks_good");
|
||||
editor.execute("review.edit 1");
|
||||
type_text(&mut editor, "A unsaved");
|
||||
editor.handle(Input::Escape);
|
||||
editor.execute("w");
|
||||
assert!(editor.message().contains("changed in another session"));
|
||||
assert_eq!(editor.context().text, "Saved feedback unsaved");
|
||||
assert!(editor.current_review_dirty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_comments_reject_multiple_carets_and_roundtrip_json_with_empty_lines() {
|
||||
let directory = Directory::new();
|
||||
let (mut editor, root, _) = source_review_editor(&directory, "one\n\n");
|
||||
set_carets(&mut editor, &[(0, 0), (1, 0)], 0);
|
||||
editor.execute("comment");
|
||||
assert!(editor.message().contains("one caret"));
|
||||
assert!(editor.review_panel().is_none());
|
||||
set_carets(&mut editor, &[(1, 0)], 0);
|
||||
editor.execute("comment");
|
||||
type_text(&mut editor, "iEmpty line");
|
||||
editor.handle(Input::Escape);
|
||||
let export = root.join("review.json");
|
||||
editor.execute(&format!("review.save {}", export.display()));
|
||||
let exported = crate::review::Review::load(&export, &root).unwrap();
|
||||
assert_eq!(exported.comments[0].anchor.excerpt(), "\n");
|
||||
assert_eq!(exported.comments[0].body, "Empty line");
|
||||
editor.handle(Input::Escape);
|
||||
editor.execute(&format!("review.open {}", export.display()));
|
||||
assert_eq!(
|
||||
editor.local_review.document.comments[0].anchor,
|
||||
exported.comments[0].anchor
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsaved_review_comments_are_discarded_on_escape() {
|
||||
let directory = Directory::new();
|
||||
@@ -3151,15 +3305,7 @@ fn unsaved_review_comments_are_discarded_on_escape() {
|
||||
type_text(&mut editor, "iDiscard me");
|
||||
editor.handle(Input::Escape);
|
||||
editor.handle(Input::Escape);
|
||||
assert!(
|
||||
editor
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments
|
||||
.is_empty()
|
||||
);
|
||||
assert!(editor.local_review.document.comments.is_empty());
|
||||
assert!(
|
||||
crate::review::Store::open(&storage)
|
||||
.unwrap()
|
||||
@@ -3299,7 +3445,7 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() {
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.revisions[1] = "working:old".into();
|
||||
editor.diff.as_mut().unwrap().local_review.document = crate::review::Review::new(root);
|
||||
editor.local_review.document = crate::review::Review::new(root);
|
||||
let anchor = Anchor {
|
||||
revision: "working:old".into(),
|
||||
path: "file.txt".into(),
|
||||
@@ -3308,19 +3454,12 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() {
|
||||
end: TextPosition { row: 1, column: 13 },
|
||||
content: editor.lines().join("\n"),
|
||||
};
|
||||
editor
|
||||
.diff
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments
|
||||
.push(Comment {
|
||||
id: 1,
|
||||
anchor: anchor.clone(),
|
||||
body: "Feedback".into(),
|
||||
resolved: false,
|
||||
});
|
||||
editor.local_review.document.comments.push(Comment {
|
||||
id: 1,
|
||||
anchor: anchor.clone(),
|
||||
body: "Feedback".into(),
|
||||
resolved: false,
|
||||
});
|
||||
editor
|
||||
.apply(vec![Effect::Replace(
|
||||
"inserted\nheader\nprefix unique target\ntail\n".into(),
|
||||
@@ -3339,10 +3478,7 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() {
|
||||
let owner = editor.diff_owner.unwrap();
|
||||
editor.select_pane(owner);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 2, column: 14 });
|
||||
assert_eq!(
|
||||
editor.diff.as_ref().unwrap().local_review.document.comments[0].anchor,
|
||||
anchor
|
||||
);
|
||||
assert_eq!(editor.local_review.document.comments[0].anchor, anchor);
|
||||
|
||||
editor
|
||||
.apply(vec![Effect::Replace(
|
||||
@@ -3355,10 +3491,7 @@ fn review_navigation_maps_working_ranges_and_preserves_stale_snapshots() {
|
||||
assert!(editor.message().contains("stale"));
|
||||
editor.select_pane(owner);
|
||||
assert_eq!(editor.cursor(), Cursor::default());
|
||||
assert_eq!(
|
||||
editor.diff.as_ref().unwrap().local_review.document.comments[0].anchor,
|
||||
anchor
|
||||
);
|
||||
assert_eq!(editor.local_review.document.comments[0].anchor, anchor);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3388,13 +3521,7 @@ fn sqlite_reviews_autosave_restore_select_and_keep_exports_independent() {
|
||||
);
|
||||
first.execute("w");
|
||||
first.handle(Input::Escape);
|
||||
let id = first
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_id
|
||||
.unwrap();
|
||||
let id = first.local_review.document.storage_id.unwrap();
|
||||
let store = Store::open(&storage).unwrap();
|
||||
let persisted = store.load(&root, id).unwrap();
|
||||
assert_eq!(persisted.comments[0].body, "First feedback");
|
||||
@@ -3437,58 +3564,30 @@ fn sqlite_reviews_autosave_restore_select_and_keep_exports_independent() {
|
||||
vec!["different revision".into()],
|
||||
vec!["changed".into()],
|
||||
);
|
||||
let restored = &reopened.workspace_diff().unwrap().local_review.document;
|
||||
let restored = &reopened.local_review.document;
|
||||
assert_eq!(restored.storage_id, Some(id));
|
||||
assert_eq!(restored.comments[0].body, "First feedback revised");
|
||||
assert!(
|
||||
reopened
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.review_anchor_range(&restored.comments[0].anchor)
|
||||
.review_anchor_range(&restored.comments[0].anchor, &reopened.local_review)
|
||||
.is_none()
|
||||
);
|
||||
reopened.execute("diff.review.new");
|
||||
let second_id = reopened
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_id
|
||||
.unwrap();
|
||||
let second_id = reopened.local_review.document.storage_id.unwrap();
|
||||
assert_ne!(id, second_id);
|
||||
let mut choosing = editor();
|
||||
choosing.set_review_directory(storage);
|
||||
historical_diff(&mut choosing, &root, vec!["old".into()], vec!["new".into()]);
|
||||
choosing.handle(Input::Enter);
|
||||
assert_eq!(
|
||||
choosing
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_id,
|
||||
Some(second_id)
|
||||
);
|
||||
assert_eq!(choosing.local_review.document.storage_id, Some(second_id));
|
||||
choosing.execute(&format!("diff.review.open {id}"));
|
||||
assert_eq!(
|
||||
choosing
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_id,
|
||||
Some(id)
|
||||
);
|
||||
assert_eq!(choosing.local_review.document.storage_id, Some(id));
|
||||
choosing.execute("diff.review.delete 1");
|
||||
assert!(store.load(&root, id).unwrap().comments.is_empty());
|
||||
choosing.execute(&format!("diff.review.open {}", exported.display()));
|
||||
let imported_id = choosing
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_id
|
||||
.unwrap();
|
||||
let imported_id = choosing.local_review.document.storage_id.unwrap();
|
||||
assert_ne!(imported_id, id);
|
||||
assert_eq!(
|
||||
store.load(&root, imported_id).unwrap().comments[0].body,
|
||||
@@ -3523,7 +3622,7 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors(
|
||||
.contains("Comment #1 · resolved · current")
|
||||
);
|
||||
let store = Store::open(&storage).unwrap();
|
||||
let document = &first.workspace_diff().unwrap().local_review.document;
|
||||
let document = &first.local_review.document;
|
||||
let id = document.storage_id.unwrap();
|
||||
let anchor = document.comments[0].anchor.clone();
|
||||
assert!(store.load(&root, id).unwrap().comments[0].resolved);
|
||||
@@ -3557,25 +3656,8 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors(
|
||||
assert!(!store.load(&root, id).unwrap().comments[0].resolved);
|
||||
stale.execute("diff.review.unresolve 1");
|
||||
assert!(stale.current_review_dirty());
|
||||
assert!(
|
||||
!stale
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments[0]
|
||||
.resolved
|
||||
);
|
||||
assert_eq!(
|
||||
stale
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments[0]
|
||||
.anchor,
|
||||
anchor
|
||||
);
|
||||
assert!(!stale.local_review.document.comments[0].resolved);
|
||||
assert_eq!(stale.local_review.document.comments[0].anchor, anchor);
|
||||
assert!(stale.message().contains("changed in another session"));
|
||||
first.execute("diff.review.resolve");
|
||||
assert!(
|
||||
@@ -3592,7 +3674,7 @@ fn review_resolution_persists_without_hiding_comments_or_changing_stale_anchors(
|
||||
.contains("Comment #1 · unresolved · current")
|
||||
);
|
||||
first.execute(&format!("diff.review.open {}", export.display()));
|
||||
let imported = &first.workspace_diff().unwrap().local_review.document;
|
||||
let imported = &first.local_review.document;
|
||||
assert_ne!(imported.storage_id, Some(id));
|
||||
assert!(
|
||||
store
|
||||
@@ -3617,34 +3699,15 @@ fn sqlite_review_conflicts_and_write_failures_keep_dirty_drafts() {
|
||||
first.handle(Input::Escape);
|
||||
first.execute("w");
|
||||
first.handle(Input::Escape);
|
||||
let id = first
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_id
|
||||
.unwrap();
|
||||
let id = first.local_review.document.storage_id.unwrap();
|
||||
let mut stale = editor();
|
||||
stale.set_review_directory(storage.clone());
|
||||
historical_diff(&mut stale, &root, vec!["old".into()], vec!["new".into()]);
|
||||
let version = stale
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_revision;
|
||||
let version = stale.local_review.document.storage_revision;
|
||||
first.execute("diff.review.verdict looks_good");
|
||||
stale.execute("diff.review.verdict changes_requested");
|
||||
assert!(stale.current_review_dirty());
|
||||
assert_eq!(
|
||||
stale
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.storage_revision,
|
||||
version
|
||||
);
|
||||
assert_eq!(stale.local_review.document.storage_revision, version);
|
||||
assert_eq!(
|
||||
Store::open(&storage)
|
||||
.unwrap()
|
||||
@@ -3665,13 +3728,7 @@ fn sqlite_review_conflicts_and_write_failures_keep_dirty_drafts() {
|
||||
first.handle(Input::Escape);
|
||||
assert!(!first.current_review_dirty());
|
||||
assert_eq!(
|
||||
first
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments[0]
|
||||
.body,
|
||||
first.local_review.document.comments[0].body,
|
||||
"Stored feedback"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(blocked).unwrap(), "preserve");
|
||||
@@ -3843,7 +3900,7 @@ fn precise_review_ranges_capture_graphemes_and_navigate_to_their_start() {
|
||||
root.join("range.json").display()
|
||||
));
|
||||
let diff = editor.workspace_diff().unwrap();
|
||||
let anchor = &diff.local_review.document.comments[0].anchor;
|
||||
let anchor = &editor.local_review.document.comments[0].anchor;
|
||||
let (start, end) = if keys == "llvj" {
|
||||
(
|
||||
TextPosition { row: 0, column: 2 },
|
||||
@@ -3857,7 +3914,10 @@ fn precise_review_ranges_capture_graphemes_and_navigate_to_their_start() {
|
||||
};
|
||||
assert_eq!(anchor.start, start);
|
||||
assert_eq!(anchor.end, end);
|
||||
assert!(diff.review_anchor_range(anchor).is_some());
|
||||
assert!(
|
||||
diff.review_anchor_range(anchor, &editor.local_review)
|
||||
.is_some()
|
||||
);
|
||||
editor.handle(Input::Escape);
|
||||
assert_eq!(editor.pane_id(), owner);
|
||||
assert_eq!(
|
||||
@@ -3881,13 +3941,7 @@ fn precise_review_ranges_capture_graphemes_and_navigate_to_their_start() {
|
||||
"diff.review.save {}",
|
||||
root.join("range.json").display()
|
||||
));
|
||||
let anchor = &editor
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments[0]
|
||||
.anchor;
|
||||
let anchor = &editor.local_review.document.comments[0].anchor;
|
||||
assert_eq!(anchor.start, TextPosition { row: 0, column: 0 });
|
||||
assert_eq!(
|
||||
anchor.end,
|
||||
@@ -3975,7 +4029,7 @@ fn local_review_visual_commands_and_palette_preserve_both_side_ranges() {
|
||||
root.join("range.json").display()
|
||||
));
|
||||
let diff = editor.workspace_diff().unwrap();
|
||||
let anchor = diff.local_review.document.comments[0].anchor.clone();
|
||||
let anchor = editor.local_review.document.comments[0].anchor.clone();
|
||||
assert_eq!(anchor.start, TextPosition { row: 0, column: 0 });
|
||||
assert_eq!(anchor.end, TextPosition { row: 2, column: 0 });
|
||||
assert_eq!(
|
||||
@@ -3986,7 +4040,10 @@ fn local_review_visual_commands_and_palette_preserve_both_side_ranges() {
|
||||
Side::After
|
||||
}
|
||||
);
|
||||
assert!(diff.review_anchor_range(&anchor).is_some());
|
||||
assert!(
|
||||
diff.review_anchor_range(&anchor, &editor.local_review)
|
||||
.is_some()
|
||||
);
|
||||
let source_pane = editor.diff_owner.unwrap();
|
||||
editor.select_pane(source_pane);
|
||||
editor
|
||||
@@ -4000,16 +4057,7 @@ fn local_review_visual_commands_and_palette_preserve_both_side_ranges() {
|
||||
editor.execute("diff.review.overview");
|
||||
assert!(editor.context().text.contains("stale"));
|
||||
editor.execute("diff.review.edit 1");
|
||||
assert_eq!(
|
||||
editor
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments[0]
|
||||
.anchor,
|
||||
anchor
|
||||
);
|
||||
assert_eq!(editor.local_review.document.comments[0].anchor, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4045,15 +4093,7 @@ fn local_review_guards_metadata_and_hidden_native_drafts_against_loss() {
|
||||
let invalid = root.join("missing/review.json");
|
||||
editor.execute(&format!("diff.review.save {}", invalid.display()));
|
||||
assert!(editor.local_reviews_dirty());
|
||||
assert!(
|
||||
editor
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments
|
||||
.is_empty()
|
||||
);
|
||||
assert!(editor.local_review.document.comments.is_empty());
|
||||
assert!(
|
||||
editor
|
||||
.parked
|
||||
@@ -7633,14 +7673,7 @@ mod system_clipboard {
|
||||
}
|
||||
editor.execute("diff.review.copy unexpected");
|
||||
editor.with_pane(editor.diff_owner.unwrap(), |editor| {
|
||||
editor
|
||||
.diff
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments
|
||||
.clear();
|
||||
editor.local_review.document.comments.clear();
|
||||
});
|
||||
editor.execute("diff.review.copy");
|
||||
if mode == "system" {
|
||||
|
||||
@@ -626,6 +626,33 @@ fn config_leader_defaults_custom_keys_validation_and_required_wire_field() {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_review_actions_and_diff_command_aliases_share_the_core_catalog() {
|
||||
let fixture = Fixture::new();
|
||||
let path = fixture.file(
|
||||
"config.rb",
|
||||
r#"
|
||||
Vamp.configure do |config|
|
||||
config.bind "C", config.core.comment
|
||||
config.bind "N", config.core.review.next
|
||||
config.bind "S", config.core.review.save
|
||||
raise "missing diff comment alias" unless Vamp.builtin_command?("diff.comment")
|
||||
raise "missing diff review alias" unless Vamp.builtin_command?("diff.review.edit")
|
||||
raise "mutable review action" unless config.core.review.frozen? && config.core.comment.frozen?
|
||||
end
|
||||
"#,
|
||||
);
|
||||
let loaded = load_config(&path).unwrap();
|
||||
let actions: Vec<_> = loaded
|
||||
.bindings
|
||||
.iter()
|
||||
.filter_map(|binding| binding.command.as_deref())
|
||||
.collect();
|
||||
assert!(actions.contains(&"core.comment"));
|
||||
assert!(actions.contains(&"core.review.next"));
|
||||
assert!(actions.contains(&"core.review.save"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_gutter_config_modes_default_and_rejection() {
|
||||
use crate::gutter::GutterStyle;
|
||||
|
||||
+115
-26
@@ -2951,6 +2951,62 @@ fn diff_status_layouts_hide_absent_panes_and_leave_unchanged_renames_blank() {
|
||||
assert!(text.contains("After · M"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_review_markers_highlight_exact_text_and_reject_stale_ranges() {
|
||||
use crate::editor::Effect;
|
||||
use crate::plugin::TextPosition;
|
||||
use crate::review::{Anchor, Comment, Review, Side};
|
||||
for wrapped in [false, true] {
|
||||
let directory = Directory::new();
|
||||
let text = format!("aé界{}\nsecond\n", " trailing".repeat(10));
|
||||
let path = directory.file("source.txt", &text);
|
||||
let mut editor = Editor::open(Some(path), vec!["text".into()]).unwrap();
|
||||
editor.local_review.document = Review::new(directory.0.canonicalize().unwrap());
|
||||
editor.local_review.document.comments.push(Comment {
|
||||
id: 1,
|
||||
anchor: Anchor {
|
||||
revision: "working:source-snapshot".into(),
|
||||
path: "source.txt".into(),
|
||||
side: Side::After,
|
||||
start: TextPosition { row: 0, column: 1 },
|
||||
end: TextPosition { row: 0, column: 3 },
|
||||
content: text,
|
||||
},
|
||||
body: "Feedback".into(),
|
||||
resolved: false,
|
||||
});
|
||||
editor.local_review.selected = Some(1);
|
||||
if wrapped {
|
||||
editor.execute("word_wrap.toggle");
|
||||
}
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(40, 16)).unwrap();
|
||||
let mut views = BufferViewports::default();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(screen_text(&terminal).contains('●'));
|
||||
assert!(
|
||||
terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.filter(|cell| cell.bg == Color::Magenta)
|
||||
.count()
|
||||
>= 2
|
||||
);
|
||||
let anchor = editor.local_review.document.comments[0].anchor.clone();
|
||||
editor
|
||||
.apply(vec![Effect::Replace("changed entirely\n".into())])
|
||||
.unwrap();
|
||||
terminal
|
||||
.draw(|frame| views.draw(frame, &mut editor))
|
||||
.unwrap();
|
||||
assert!(!screen_text(&terminal).contains('●'));
|
||||
assert_eq!(editor.local_review.document.comments[0].anchor, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_diff(
|
||||
terminal: &mut Terminal<ratatui::backend::TestBackend>,
|
||||
diff: &DiffState,
|
||||
@@ -2959,12 +3015,44 @@ fn render_diff(
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
panels.begin_frame();
|
||||
draw_diff(frame, diff, frame.area(), "↑/↓", "h/j/k/l", panels, None);
|
||||
draw_diff(
|
||||
frame,
|
||||
diff,
|
||||
frame.area(),
|
||||
"↑/↓",
|
||||
"h/j/k/l",
|
||||
panels,
|
||||
None,
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn commented_diff() -> DiffState {
|
||||
fn render_review_diff(
|
||||
terminal: &mut Terminal<ratatui::backend::TestBackend>,
|
||||
diff: &DiffState,
|
||||
review: &crate::review::ReviewState,
|
||||
panels: &mut WheelPanels,
|
||||
) {
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
panels.begin_frame();
|
||||
draw_diff(
|
||||
frame,
|
||||
diff,
|
||||
frame.area(),
|
||||
"↑/↓",
|
||||
"h/j/k/l",
|
||||
panels,
|
||||
None,
|
||||
Some(review),
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn commented_diff() -> (DiffState, crate::review::ReviewState) {
|
||||
use crate::review::{Anchor, Comment, Side};
|
||||
let before = format!(
|
||||
"{}\nold e\u{301}界 second\nkeep",
|
||||
@@ -2980,8 +3068,9 @@ fn commented_diff() -> DiffState {
|
||||
file.old_path = Some("old.txt".into());
|
||||
let mut diff = diff_comparison(vec![file]);
|
||||
diff.files_visible = false;
|
||||
diff.local_review.document.root = diff.comparison.as_ref().unwrap().root.clone();
|
||||
diff.local_review.document.comments = vec![
|
||||
let mut review = crate::review::ReviewState::default();
|
||||
review.document.root = diff.comparison.as_ref().unwrap().root.clone();
|
||||
review.document.comments = vec![
|
||||
Comment {
|
||||
id: 1,
|
||||
anchor: Anchor {
|
||||
@@ -3009,8 +3098,8 @@ fn commented_diff() -> DiffState {
|
||||
resolved: false,
|
||||
},
|
||||
];
|
||||
diff.local_review.selected = Some(1);
|
||||
diff
|
||||
review.selected = Some(1);
|
||||
(diff, review)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -3022,14 +3111,13 @@ fn local_review_highlights_exact_graphemes_newlines_clips_and_wrap_continuations
|
||||
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();
|
||||
let (mut diff, mut review) = 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
|
||||
let comment = review
|
||||
.document
|
||||
.comments
|
||||
.iter_mut()
|
||||
@@ -3044,11 +3132,11 @@ fn local_review_highlights_exact_graphemes_newlines_clips_and_wrap_continuations
|
||||
column: end.1,
|
||||
};
|
||||
let anchor = comment.anchor.clone();
|
||||
diff.local_review.selected = Some(comment.id);
|
||||
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);
|
||||
render_review_diff(&mut terminal, &diff, &review, &mut panels);
|
||||
let file = &diff.comparison.as_ref().unwrap().files[0];
|
||||
let wrapped = panels
|
||||
.diff_wrap
|
||||
@@ -3164,13 +3252,13 @@ fn local_review_highlights_exact_graphemes_newlines_clips_and_wrap_continuations
|
||||
fn local_review_markers_preserve_diff_signs_ranges_and_wrapped_geometry() {
|
||||
for mode in [ViewMode::Split, ViewMode::Unified] {
|
||||
for wrapping in [false, true] {
|
||||
let mut diff = commented_diff();
|
||||
let (mut diff, review) = commented_diff();
|
||||
diff.set_view_mode(mode);
|
||||
diff.top = 0;
|
||||
diff.word_wrap = wrapping;
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(90, 40)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
render_review_diff(&mut terminal, &diff, &review, &mut panels);
|
||||
let buffer = terminal.backend().buffer();
|
||||
let markers: Vec<_> = buffer
|
||||
.content
|
||||
@@ -3219,20 +3307,20 @@ fn local_review_markers_preserve_diff_signs_ranges_and_wrapped_geometry() {
|
||||
fn local_review_markers_reject_stale_identity_text_and_rename_paths() {
|
||||
for mode in [ViewMode::Split, ViewMode::Unified] {
|
||||
for mismatch in ["revision", "content", "path", "side", "root"] {
|
||||
let mut diff = commented_diff();
|
||||
let (mut diff, mut review) = commented_diff();
|
||||
diff.set_view_mode(mode);
|
||||
diff.local_review.document.comments.truncate(1);
|
||||
let anchor = &mut diff.local_review.document.comments[0].anchor;
|
||||
review.document.comments.truncate(1);
|
||||
let anchor = &mut review.document.comments[0].anchor;
|
||||
match mismatch {
|
||||
"revision" => anchor.revision.push_str("-newer"),
|
||||
"content" => anchor.content.replace_range(..1, "X"),
|
||||
"path" => anchor.path = "new.txt".into(),
|
||||
"side" => anchor.side = crate::review::Side::After,
|
||||
"root" => diff.local_review.document.root = "another-repository".into(),
|
||||
"root" => review.document.root = "another-repository".into(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
let mut terminal = Terminal::new(ratatui::backend::TestBackend::new(90, 20)).unwrap();
|
||||
render_diff(&mut terminal, &diff, &mut WheelPanels::default());
|
||||
render_review_diff(&mut terminal, &diff, &review, &mut WheelPanels::default());
|
||||
assert!(
|
||||
terminal
|
||||
.backend()
|
||||
@@ -3258,18 +3346,18 @@ fn local_review_markers_reject_stale_identity_text_and_rename_paths() {
|
||||
#[test]
|
||||
fn unified_context_shows_comments_from_either_snapshot_side() {
|
||||
for side in [crate::review::Side::Before, crate::review::Side::After] {
|
||||
let mut diff = commented_diff();
|
||||
let (mut diff, mut review) = commented_diff();
|
||||
diff.set_view_mode(ViewMode::Unified);
|
||||
diff.local_review
|
||||
review
|
||||
.document
|
||||
.comments
|
||||
.retain(|comment| comment.anchor.side == side);
|
||||
let comment = &mut diff.local_review.document.comments[0];
|
||||
let comment = &mut review.document.comments[0];
|
||||
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);
|
||||
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());
|
||||
render_review_diff(&mut terminal, &diff, &review, &mut WheelPanels::default());
|
||||
assert!(
|
||||
screen_text(&terminal).contains("●keep"),
|
||||
"{side:?}: {}",
|
||||
@@ -3296,13 +3384,13 @@ fn unified_context_shows_comments_from_either_snapshot_side() {
|
||||
fn local_review_markers_yield_to_text_on_narrow_surfaces() {
|
||||
for mode in [ViewMode::Split, ViewMode::Unified] {
|
||||
for (width, height) in [(1, 1), (2, 2), (3, 3), (8, 4), (16, 8), (30, 12)] {
|
||||
let mut diff = commented_diff();
|
||||
let (mut diff, review) = commented_diff();
|
||||
diff.set_view_mode(mode);
|
||||
diff.word_wrap = true;
|
||||
let mut terminal =
|
||||
Terminal::new(ratatui::backend::TestBackend::new(width, height)).unwrap();
|
||||
let mut panels = WheelPanels::default();
|
||||
render_diff(&mut terminal, &diff, &mut panels);
|
||||
render_review_diff(&mut terminal, &diff, &review, &mut panels);
|
||||
for (area, focus) in panels
|
||||
.diff
|
||||
.as_ref()
|
||||
@@ -3702,6 +3790,7 @@ fn pull_request_review_symbols_use_terminal_cells_and_keep_the_selection_arrow_i
|
||||
"h/j/k/l",
|
||||
&mut panels,
|
||||
with_editor.then_some(&editor),
|
||||
None,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
@@ -4834,7 +4923,7 @@ fn unavailable_file_documents_show_the_actual_error_in_managed_workspaces() {
|
||||
};
|
||||
terminal
|
||||
.draw(|frame| {
|
||||
panels.draw_documents(frame, &diff, frame.area(), None, None);
|
||||
panels.draw_documents(frame, &diff, frame.area(), None, None, None);
|
||||
})
|
||||
.unwrap();
|
||||
let text = screen_text(&terminal);
|
||||
|
||||
Reference in New Issue
Block a user