fix: save review comments only on explicit write
This commit is contained in:
+114
-25
@@ -19,6 +19,7 @@ pub(super) struct ReviewBuffer {
|
||||
owner: usize,
|
||||
kind: ReviewText,
|
||||
title: String,
|
||||
pending_comment: Option<Comment>,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
@@ -108,6 +109,7 @@ impl Editor {
|
||||
.as_ref()
|
||||
.is_some_and(|binding| binding.owner == self.tab_id)
|
||||
{
|
||||
buffer.review_buffer.as_mut().unwrap().pending_comment = None;
|
||||
buffer.saved_lines.clone_from(&buffer.lines);
|
||||
buffer.dirty_cache.set(Some(false));
|
||||
}
|
||||
@@ -115,11 +117,13 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub(super) fn autosave_local_review(&mut self) -> Result<(), String> {
|
||||
self.sync_review_text();
|
||||
let Some(directory) = self.review_directory.as_deref() else {
|
||||
return Ok(());
|
||||
};
|
||||
if !self.current_review_dirty() {
|
||||
if !self
|
||||
.workspace_diff()
|
||||
.is_some_and(|diff| diff.local_review.dirty)
|
||||
{
|
||||
return Ok(());
|
||||
}
|
||||
let mut store = crate::review::Store::open(directory)?;
|
||||
@@ -130,7 +134,7 @@ impl Editor {
|
||||
.local_review
|
||||
.document,
|
||||
)?;
|
||||
self.mark_review_saved();
|
||||
self.review_diff_mut().unwrap().local_review.dirty = false;
|
||||
Ok(())
|
||||
}
|
||||
pub fn review_panel_title(&self) -> Option<&str> {
|
||||
@@ -153,8 +157,18 @@ impl Editor {
|
||||
.origin
|
||||
.expect("review panel has a source pane");
|
||||
self.finish_pane_interaction();
|
||||
if let Err(error) = self.autosave_local_review() {
|
||||
self.set_message(format!("Review not saved: {error}"));
|
||||
if let Some(mut binding) = self.active.review_buffer.take() {
|
||||
let text = self.active.saved_lines.join("\n");
|
||||
let wrap = self.active.view.word_wrap;
|
||||
let mut buffer = Buffer::from_text(self.active.id, None, None, &text, None)
|
||||
.expect("saved review text is valid");
|
||||
buffer.view.word_wrap = wrap;
|
||||
buffer.resolver_readonly = self.active.resolver_readonly;
|
||||
if binding.pending_comment.take().is_some() {
|
||||
self.review_diff_mut().unwrap().local_review.selected = None;
|
||||
}
|
||||
buffer.review_buffer = Some(binding);
|
||||
self.active = buffer;
|
||||
}
|
||||
self.select_pane(source);
|
||||
self.bind_diff_buffer();
|
||||
@@ -242,7 +256,7 @@ impl Editor {
|
||||
),
|
||||
ReviewSource::New => (Review::new(root), None, true),
|
||||
};
|
||||
// Retire old identities before autosave can synchronize their native drafts.
|
||||
// Retire old drafts before comment IDs can be reused.
|
||||
self.detach_review_buffers();
|
||||
let state = &mut self.review_diff_mut().unwrap().local_review;
|
||||
state.document = document;
|
||||
@@ -276,7 +290,7 @@ impl Editor {
|
||||
self.open_review_overview()
|
||||
}
|
||||
|
||||
fn sync_review_text(&mut self) {
|
||||
fn sync_review_text(&mut self) -> Vec<(ReviewText, Option<String>)> {
|
||||
let owner = self.tab_id;
|
||||
let updates: Vec<_> = std::iter::once(&self.active)
|
||||
.chain(self.parked.values())
|
||||
@@ -284,13 +298,34 @@ impl Editor {
|
||||
let binding = buffer.review_buffer.as_ref()?;
|
||||
(binding.owner == owner
|
||||
&& matches!(binding.kind, ReviewText::Comment(_) | ReviewText::Summary))
|
||||
.then(|| (binding.kind, buffer.lines.join("\n")))
|
||||
.then(|| {
|
||||
(
|
||||
binding.kind,
|
||||
buffer.lines.join("\n"),
|
||||
binding.pending_comment.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let Some(diff) = self.review_diff_mut() else {
|
||||
return;
|
||||
return Vec::new();
|
||||
};
|
||||
for (kind, text) in updates {
|
||||
let mut previous = Vec::new();
|
||||
for (kind, text, pending) in updates {
|
||||
if let Some(mut comment) = pending
|
||||
&& !diff
|
||||
.local_review
|
||||
.document
|
||||
.comments
|
||||
.iter()
|
||||
.any(|entry| entry.id == comment.id)
|
||||
{
|
||||
previous.push((kind, None));
|
||||
comment.body = text;
|
||||
diff.local_review.document.comments.push(comment);
|
||||
diff.local_review.dirty = true;
|
||||
continue;
|
||||
}
|
||||
let destination = match kind {
|
||||
ReviewText::Comment(id) => diff
|
||||
.local_review
|
||||
@@ -306,14 +341,48 @@ impl Editor {
|
||||
if let Some(destination) = destination
|
||||
&& *destination != text
|
||||
{
|
||||
*destination = text;
|
||||
let old = std::mem::replace(destination, text);
|
||||
previous.push((kind, Some(old)));
|
||||
diff.local_review.dirty = true;
|
||||
}
|
||||
}
|
||||
previous
|
||||
}
|
||||
|
||||
pub(super) fn save_local_review(&mut self, path: Option<&Path>) -> Result<(), String> {
|
||||
self.sync_review_text();
|
||||
let dirty = self
|
||||
.review_diff_mut()
|
||||
.ok_or("No comparison is open")?
|
||||
.local_review
|
||||
.dirty;
|
||||
let previous = self.sync_review_text();
|
||||
let result = self.write_local_review(path);
|
||||
if result.is_err() {
|
||||
let state = &mut self.review_diff_mut().unwrap().local_review;
|
||||
for (kind, text) in previous {
|
||||
match (kind, text) {
|
||||
(ReviewText::Comment(id), None) => {
|
||||
state.document.comments.retain(|comment| comment.id != id)
|
||||
}
|
||||
(ReviewText::Comment(id), Some(text)) => {
|
||||
state
|
||||
.document
|
||||
.comments
|
||||
.iter_mut()
|
||||
.find(|comment| comment.id == id)
|
||||
.unwrap()
|
||||
.body = text;
|
||||
}
|
||||
(ReviewText::Summary, Some(text)) => state.document.summary = text,
|
||||
_ => unreachable!("only editable review text is synchronized"),
|
||||
}
|
||||
}
|
||||
state.dirty = dirty;
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn write_local_review(&mut self, path: Option<&Path>) -> Result<(), String> {
|
||||
if self.review_directory.is_some() {
|
||||
if let Some(path) = path {
|
||||
self.review_diff_mut()
|
||||
@@ -324,6 +393,7 @@ impl Editor {
|
||||
self.review_diff_mut().unwrap().local_review.path = Some(path.to_path_buf());
|
||||
}
|
||||
self.autosave_local_review()?;
|
||||
self.mark_review_saved();
|
||||
self.set_message(if path.is_some() {
|
||||
"Review exported as JSON; autosave remains in SQLite"
|
||||
} else {
|
||||
@@ -408,7 +478,6 @@ impl Editor {
|
||||
};
|
||||
return self.replace_local_review(root, source);
|
||||
}
|
||||
self.sync_review_text();
|
||||
match action {
|
||||
Action::DiffComment => self.add_review_comment()?,
|
||||
Action::ReviewNew => self.replace_local_review(root, ReviewSource::New)?,
|
||||
@@ -612,31 +681,46 @@ impl Editor {
|
||||
.comments
|
||||
.iter()
|
||||
.map(|comment| comment.id)
|
||||
.chain(
|
||||
std::iter::once(&self.active)
|
||||
.chain(self.parked.values())
|
||||
.filter_map(|buffer| {
|
||||
let binding = buffer.review_buffer.as_ref()?;
|
||||
(binding.owner == self.tab_id)
|
||||
.then_some(binding.pending_comment.as_ref()?.id)
|
||||
}),
|
||||
)
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
.checked_add(1)
|
||||
.ok_or("Review comment IDs exhausted")?;
|
||||
self.prepare_command();
|
||||
self.finish_command_mode();
|
||||
let state = &mut self.diff.as_mut().unwrap().local_review;
|
||||
state.document.comments.push(Comment {
|
||||
let comment = Comment {
|
||||
id,
|
||||
anchor,
|
||||
body: String::new(),
|
||||
resolved: false,
|
||||
});
|
||||
state.dirty = true;
|
||||
self.show_review_comment(id)
|
||||
};
|
||||
self.open_review_comment(id, Some(&comment))?;
|
||||
self.active.review_buffer.as_mut().unwrap().pending_comment = Some(comment);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn show_review_comment(&mut self, id: u64) -> Result<(), String> {
|
||||
self.open_review_comment(id, None)
|
||||
}
|
||||
|
||||
fn open_review_comment(&mut self, id: u64, draft: Option<&Comment>) -> Result<(), String> {
|
||||
let diff = self.workspace_diff().unwrap();
|
||||
let comment = diff
|
||||
.local_review
|
||||
.document
|
||||
.comments
|
||||
.iter()
|
||||
.find(|comment| comment.id == id)
|
||||
let comment = draft
|
||||
.or_else(|| {
|
||||
diff.local_review
|
||||
.document
|
||||
.comments
|
||||
.iter()
|
||||
.find(|comment| comment.id == id)
|
||||
})
|
||||
.ok_or("Comment no longer exists")?;
|
||||
let text = comment.body.clone();
|
||||
let anchor = &comment.anchor;
|
||||
@@ -668,6 +752,7 @@ impl Editor {
|
||||
})
|
||||
.flatten()
|
||||
});
|
||||
let start = anchor.start;
|
||||
let owner = self.diff_owner.unwrap();
|
||||
self.select_pane(owner);
|
||||
let diff = self.diff.as_mut().unwrap();
|
||||
@@ -677,6 +762,9 @@ impl Editor {
|
||||
diff.local_review.pending_navigation = Some(id);
|
||||
}
|
||||
self.finish_review_navigation();
|
||||
if draft.is_some() {
|
||||
self.place_navigation(start);
|
||||
}
|
||||
self.open_review_buffer(ReviewText::Comment(id), &text, title)?;
|
||||
if target.is_none() {
|
||||
self.set_message("Comment is stale; original snapshot anchor preserved");
|
||||
@@ -861,13 +949,14 @@ impl Editor {
|
||||
owner: self.tab_id,
|
||||
kind,
|
||||
title,
|
||||
pending_comment: None,
|
||||
});
|
||||
buffer.resolver_readonly = matches!(kind, ReviewText::Overview | ReviewText::Reviews);
|
||||
buffer.view.word_wrap = true;
|
||||
self.add_buffer(buffer);
|
||||
}
|
||||
self.set_message(if self.review_directory.is_some() {
|
||||
"Local review · dismiss to commit · :w saves · :diff.review.save path exports JSON"
|
||||
"Local review · :w saves · Escape discards unsaved text · :diff.review.save path exports JSON"
|
||||
} else {
|
||||
"Local review · :w saves JSON; first choose :diff.review.save <path>"
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user