feat: persist local reviews automatically in SQLite
This commit is contained in:
+253
@@ -990,6 +990,12 @@ core_actions![
|
||||
true,
|
||||
true
|
||||
),
|
||||
(
|
||||
"core.diff.review.new",
|
||||
Action::ReviewNew,
|
||||
&[Mode::Normal],
|
||||
true
|
||||
),
|
||||
(
|
||||
"core.word_wrap.toggle",
|
||||
Action::WordWrapToggle,
|
||||
@@ -2072,6 +2078,7 @@ pub struct Editor {
|
||||
render_pane_focused: bool,
|
||||
diff_owner: Option<usize>,
|
||||
parked_diff: Option<DiffState>,
|
||||
review_directory: Option<PathBuf>,
|
||||
diff_pane_bounds: [(Focus, PaneRect); 3],
|
||||
// Closed destinations stay until bounded eviction so they cannot shift jump_index.
|
||||
jumps: VecDeque<JumpLocation>,
|
||||
@@ -2178,6 +2185,7 @@ impl Editor {
|
||||
render_pane_focused: true,
|
||||
diff_owner: None,
|
||||
parked_diff: None,
|
||||
review_directory: None,
|
||||
diff_pane_bounds: [(
|
||||
Focus::Files,
|
||||
PaneRect {
|
||||
@@ -4502,6 +4510,9 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
self.finish_review_navigation();
|
||||
if let Err(error) = self.initialize_local_review() {
|
||||
self.set_message(format!("Local review: {error}"));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -5250,6 +5261,7 @@ impl Editor {
|
||||
| Action::ReviewVerdict
|
||||
| Action::ReviewSave
|
||||
| Action::ReviewOpen
|
||||
| Action::ReviewNew
|
||||
| Action::WordWrapToggle
|
||||
| Action::GitBlameToggle
|
||||
| Action::Quit
|
||||
@@ -8448,6 +8460,7 @@ impl Editor {
|
||||
| Action::ReviewVerdict
|
||||
| Action::ReviewSave
|
||||
| Action::ReviewOpen
|
||||
| Action::ReviewNew
|
||||
| Action::WordWrapToggle
|
||||
| Action::GitBlameToggle
|
||||
| Action::LspDefinition
|
||||
@@ -9361,6 +9374,7 @@ impl Editor {
|
||||
| Action::ReviewVerdict
|
||||
| Action::ReviewSave
|
||||
| Action::ReviewOpen
|
||||
| Action::ReviewNew
|
||||
) {
|
||||
self.finish_prompt();
|
||||
if let Err(error) = self.local_review_command(entry.action, args) {
|
||||
@@ -9368,6 +9382,21 @@ impl Editor {
|
||||
}
|
||||
return None;
|
||||
}
|
||||
if self.review_directory.is_some()
|
||||
&& matches!(
|
||||
entry.action,
|
||||
Action::TabPrevious
|
||||
| Action::TabNext
|
||||
| Action::TabNew
|
||||
| Action::TabClose
|
||||
| Action::Quit
|
||||
| Action::Restart
|
||||
)
|
||||
&& let Err(error) = self.autosave_local_review()
|
||||
{
|
||||
self.set_message(format!("Review not saved: {error}"));
|
||||
return None;
|
||||
}
|
||||
if self.review_panel().is_some()
|
||||
&& matches!(
|
||||
entry.action,
|
||||
@@ -15911,11 +15940,235 @@ mod tests {
|
||||
}],
|
||||
})),
|
||||
);
|
||||
if editor.review_panel().is_some() {
|
||||
return;
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Before);
|
||||
assert!(editor.diff_interacting());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlite_reviews_autosave_restore_select_and_keep_exports_independent() {
|
||||
use crate::review::{Review, Store, Verdict};
|
||||
let directory = Directory::new();
|
||||
let root = directory.0.canonicalize().unwrap();
|
||||
let storage = root.join("data");
|
||||
let mut first = editor();
|
||||
first.set_review_directory(storage.clone());
|
||||
historical_diff(
|
||||
&mut first,
|
||||
&root,
|
||||
vec!["p界e\u{301}z".into()],
|
||||
vec!["changed".into()],
|
||||
);
|
||||
type_text(&mut first, "lvl:diff.comment");
|
||||
first.handle(Input::Enter);
|
||||
type_text(&mut first, "iFirst feedback");
|
||||
first.handle(Input::Escape);
|
||||
assert!(
|
||||
Store::open(&storage)
|
||||
.unwrap()
|
||||
.list(&root)
|
||||
.unwrap()
|
||||
.is_empty()
|
||||
);
|
||||
first.handle(Input::Escape);
|
||||
let id = first
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.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");
|
||||
assert_eq!(
|
||||
persisted.comments[0].anchor.start,
|
||||
TextPosition { row: 0, column: 1 }
|
||||
);
|
||||
assert_eq!(
|
||||
persisted.comments[0].anchor.end,
|
||||
TextPosition { row: 0, column: 3 }
|
||||
);
|
||||
first.execute("diff.review.summary");
|
||||
type_text(&mut first, "iSummary");
|
||||
first.handle(Input::Escape);
|
||||
first.execute("diff.review.verdict changes_requested");
|
||||
let persisted = store.load(&root, id).unwrap();
|
||||
assert_eq!(persisted.summary, "Summary");
|
||||
assert_eq!(persisted.verdict, Some(Verdict::ChangesRequested));
|
||||
let exported = root.join("shared.json");
|
||||
first.execute(&format!("diff.review.save {}", exported.display()));
|
||||
first.execute("diff.review.edit 1");
|
||||
type_text(&mut first, "A revised");
|
||||
first.handle(Input::Escape);
|
||||
first.handle(Input::Escape);
|
||||
assert_eq!(
|
||||
store.load(&root, id).unwrap().comments[0].body,
|
||||
"First feedback revised"
|
||||
);
|
||||
assert_eq!(
|
||||
Review::load(&exported, &root).unwrap().comments[0].body,
|
||||
"First feedback"
|
||||
);
|
||||
let mut reopened = editor();
|
||||
reopened.set_review_directory(storage.clone());
|
||||
historical_diff(
|
||||
&mut reopened,
|
||||
&root,
|
||||
vec!["different revision".into()],
|
||||
vec!["changed".into()],
|
||||
);
|
||||
let restored = &reopened.workspace_diff().unwrap().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_current(&restored.comments[0].anchor)
|
||||
);
|
||||
reopened.execute("diff.review.new");
|
||||
let second_id = reopened
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.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()]);
|
||||
assert_eq!(
|
||||
choosing
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.choices
|
||||
.iter()
|
||||
.map(|(id, _)| *id)
|
||||
.collect::<std::collections::BTreeSet<_>>(),
|
||||
std::collections::BTreeSet::from([id, second_id])
|
||||
);
|
||||
choosing.handle(Input::Enter);
|
||||
assert_eq!(
|
||||
choosing
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.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)
|
||||
);
|
||||
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();
|
||||
assert_ne!(imported_id, id);
|
||||
assert_eq!(
|
||||
store.load(&root, imported_id).unwrap().comments[0].body,
|
||||
"First feedback"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sqlite_review_conflicts_and_write_failures_keep_dirty_drafts() {
|
||||
use crate::review::Store;
|
||||
let directory = Directory::new();
|
||||
let root = directory.0.canonicalize().unwrap();
|
||||
let storage = root.join("data");
|
||||
let mut first = editor();
|
||||
first.set_review_directory(storage.clone());
|
||||
historical_diff(&mut first, &root, vec!["old".into()], vec!["new".into()]);
|
||||
first.execute("diff.comment");
|
||||
type_text(&mut first, "iStored feedback");
|
||||
first.handle(Input::Escape);
|
||||
first.handle(Input::Escape);
|
||||
let id = first
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.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;
|
||||
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!(
|
||||
Store::open(&storage)
|
||||
.unwrap()
|
||||
.load(&root, id)
|
||||
.unwrap()
|
||||
.verdict,
|
||||
Some(crate::review::Verdict::LooksGood)
|
||||
);
|
||||
let blocked = root.join("not-a-directory");
|
||||
fs::write(&blocked, "preserve").unwrap();
|
||||
first.set_review_directory(blocked.clone());
|
||||
first.execute("diff.review.edit 1");
|
||||
type_text(&mut first, "A unsaved");
|
||||
first.handle(Input::Escape);
|
||||
first.handle(Input::Escape);
|
||||
assert!(first.current_review_dirty());
|
||||
assert_eq!(
|
||||
first
|
||||
.workspace_diff()
|
||||
.unwrap()
|
||||
.local_review
|
||||
.document
|
||||
.comments[0]
|
||||
.body,
|
||||
"Stored feedback unsaved"
|
||||
);
|
||||
assert_eq!(fs::read_to_string(blocked).unwrap(), "preserve");
|
||||
assert_eq!(
|
||||
Store::open(&storage)
|
||||
.unwrap()
|
||||
.load(&root, id)
|
||||
.unwrap()
|
||||
.comments[0]
|
||||
.body,
|
||||
"Stored feedback"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_review_native_composition_roundtrip_and_draft_lifecycle() {
|
||||
use crate::review::{Review, Side, Verdict};
|
||||
|
||||
Reference in New Issue
Block a user