feat: support comments in ordinary source buffers
This commit is contained in:
+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" {
|
||||
|
||||
Reference in New Issue
Block a user